@cyanheads/brapi-mcp-server 0.8.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/AGENTS.md +27 -24
  2. package/CLAUDE.md +27 -24
  3. package/Dockerfile +93 -54
  4. package/README.md +6 -3
  5. package/changelog/0.8.x/0.8.1.md +38 -0
  6. package/dist/index.js +6 -5
  7. package/dist/index.js.map +1 -1
  8. package/dist/mcp-server/resources/definitions/brapi-variable.resource.d.ts.map +1 -1
  9. package/dist/mcp-server/resources/definitions/brapi-variable.resource.js +2 -10
  10. package/dist/mcp-server/resources/definitions/brapi-variable.resource.js.map +1 -1
  11. package/dist/mcp-server/tools/definitions/brapi-build-phenotype-matrix.tool.js +1 -1
  12. package/dist/mcp-server/tools/definitions/brapi-build-phenotype-matrix.tool.js.map +1 -1
  13. package/dist/mcp-server/tools/definitions/brapi-connect.tool.d.ts.map +1 -1
  14. package/dist/mcp-server/tools/definitions/brapi-connect.tool.js +1 -1
  15. package/dist/mcp-server/tools/definitions/brapi-connect.tool.js.map +1 -1
  16. package/dist/mcp-server/tools/definitions/brapi-dataframe-describe.tool.d.ts.map +1 -1
  17. package/dist/mcp-server/tools/definitions/brapi-dataframe-describe.tool.js +1 -3
  18. package/dist/mcp-server/tools/definitions/brapi-dataframe-describe.tool.js.map +1 -1
  19. package/dist/mcp-server/tools/definitions/brapi-dataframe-export.tool.d.ts.map +1 -1
  20. package/dist/mcp-server/tools/definitions/brapi-dataframe-export.tool.js +4 -10
  21. package/dist/mcp-server/tools/definitions/brapi-dataframe-export.tool.js.map +1 -1
  22. package/dist/mcp-server/tools/definitions/brapi-dataframe-query.tool.js +1 -1
  23. package/dist/mcp-server/tools/definitions/brapi-dataframe-query.tool.js.map +1 -1
  24. package/dist/mcp-server/tools/definitions/brapi-describe-filters.tool.d.ts.map +1 -1
  25. package/dist/mcp-server/tools/definitions/brapi-describe-filters.tool.js +1 -5
  26. package/dist/mcp-server/tools/definitions/brapi-describe-filters.tool.js.map +1 -1
  27. package/dist/mcp-server/tools/definitions/brapi-export-genotype-matrix.tool.d.ts.map +1 -1
  28. package/dist/mcp-server/tools/definitions/brapi-export-genotype-matrix.tool.js +2 -2
  29. package/dist/mcp-server/tools/definitions/brapi-export-genotype-matrix.tool.js.map +1 -1
  30. package/dist/mcp-server/tools/definitions/brapi-find-genotype-calls.tool.js +2 -2
  31. package/dist/mcp-server/tools/definitions/brapi-find-genotype-calls.tool.js.map +1 -1
  32. package/dist/mcp-server/tools/definitions/brapi-germplasm-performance.tool.js +1 -5
  33. package/dist/mcp-server/tools/definitions/brapi-germplasm-performance.tool.js.map +1 -1
  34. package/dist/mcp-server/tools/definitions/brapi-get-germplasm.tool.d.ts.map +1 -1
  35. package/dist/mcp-server/tools/definitions/brapi-get-germplasm.tool.js +2 -10
  36. package/dist/mcp-server/tools/definitions/brapi-get-germplasm.tool.js.map +1 -1
  37. package/dist/mcp-server/tools/definitions/brapi-get-image.tool.js +1 -1
  38. package/dist/mcp-server/tools/definitions/brapi-get-image.tool.js.map +1 -1
  39. package/dist/mcp-server/tools/definitions/brapi-get-study.tool.d.ts.map +1 -1
  40. package/dist/mcp-server/tools/definitions/brapi-get-study.tool.js +2 -10
  41. package/dist/mcp-server/tools/definitions/brapi-get-study.tool.js.map +1 -1
  42. package/dist/mcp-server/tools/definitions/brapi-raw-get.tool.d.ts.map +1 -1
  43. package/dist/mcp-server/tools/definitions/brapi-raw-get.tool.js +0 -1
  44. package/dist/mcp-server/tools/definitions/brapi-raw-get.tool.js.map +1 -1
  45. package/dist/mcp-server/tools/definitions/brapi-raw-search.tool.d.ts.map +1 -1
  46. package/dist/mcp-server/tools/definitions/brapi-raw-search.tool.js +1 -5
  47. package/dist/mcp-server/tools/definitions/brapi-raw-search.tool.js.map +1 -1
  48. package/dist/mcp-server/tools/definitions/brapi-submit-observations.tool.d.ts +5 -0
  49. package/dist/mcp-server/tools/definitions/brapi-submit-observations.tool.d.ts.map +1 -1
  50. package/dist/mcp-server/tools/definitions/brapi-submit-observations.tool.js +55 -10
  51. package/dist/mcp-server/tools/definitions/brapi-submit-observations.tool.js.map +1 -1
  52. package/dist/mcp-server/tools/shared/find-helpers.d.ts +5 -5
  53. package/dist/mcp-server/tools/shared/find-helpers.d.ts.map +1 -1
  54. package/dist/mcp-server/tools/shared/find-helpers.js +7 -11
  55. package/dist/mcp-server/tools/shared/find-helpers.js.map +1 -1
  56. package/dist/services/capability-registry/capability-registry.d.ts.map +1 -1
  57. package/dist/services/capability-registry/capability-registry.js +6 -6
  58. package/dist/services/capability-registry/capability-registry.js.map +1 -1
  59. package/dist/services/server-registry/server-registry.d.ts.map +1 -1
  60. package/dist/services/server-registry/server-registry.js +0 -3
  61. package/dist/services/server-registry/server-registry.js.map +1 -1
  62. package/manifest.json +1 -1
  63. package/package.json +11 -10
  64. package/server.json +11 -13
package/AGENTS.md CHANGED
@@ -1,10 +1,11 @@
1
1
  # Agent Protocol
2
2
 
3
3
  **Server:** brapi-mcp-server
4
- **Version:** 0.8.0
5
- **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.6`
4
+ **Version:** 0.8.1
5
+ **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.11`
6
6
  **Engines:** Bun ≥1.4.0, Node ≥24.0.0
7
- **MCP SDK:** `@modelcontextprotocol/server` ^2.0.0 (protocol revisions 2026-07-28 and 2025-*)
7
+ **MCP SDK:** `@modelcontextprotocol/server` ^2.2.0 (protocol revisions 2026-07-28 and 2025-*)
8
+ **Zod:** ^4.6.5
8
9
 
9
10
  > **Read the framework docs first:** `node_modules/@cyanheads/mcp-ts-core/CLAUDE.md` contains the full API reference — builders, Context, error codes, exports, patterns. This file covers server-specific conventions only.
10
11
 
@@ -36,6 +37,7 @@ Tailor suggestions to what's actually missing or stale — don't recite the full
36
37
  - **Use `ctx.state`** for tenant-scoped storage. Never access persistence directly.
37
38
  - **Need input the caller didn't supply?** `return ctx.requestInput(...)` and read `ctx.inputs` when the handler is re-entered. Never `await` for user input mid-handler.
38
39
  - **Secrets in env vars only** — never hardcoded.
40
+ - **Cut noise.** Add only what earns its place: no speculative generality, no guards for states the framework already prevents (Zod-validated params, classified errors), no abstraction until a third caller proves it, no option nothing sets.
39
41
  - **Close the loop on issues.** When implementing work tracked by a GitHub issue, comment on the issue with what landed and close it. Do both — a comment without a close leaves stale issues open; a close without a comment leaves no record of what shipped. The comment is for future readers — state the concrete changes, not the conversation that produced them.
40
42
 
41
43
  ---
@@ -175,10 +177,10 @@ Handlers receive a unified `ctx` object. Currently used surface:
175
177
  | `ctx.signal` | `AbortSignal` — threaded into every BrAPI HTTP call so client-side cancellation aborts the upstream request. |
176
178
  | `ctx.requestId` | Unique request ID — auto-attached to every `ctx.log` entry. |
177
179
  | `ctx.tenantId` | Tenant ID from JWT or `'default'` for stdio / HTTP+`auth=none` — outer scope on all `ctx.state` reads/writes. |
178
- | `ctx.requestInput` / `ctx.inputs` | Multi-round-trip input. `brapi_submit_observations` gates apply-mode writes on a `confirm` elicitation: it reads `ctx.inputs.view('confirm')`, returns `ctx.requestInput({ inputRequests: … })` when the answer is missing, and treats a declined, cancelled, or unparseable answer as terminal (`user_declined`). `force: true` skips the round. |
180
+ | `ctx.requestInput` / `ctx.inputs` | Multi-round-trip input. `brapi_submit_observations` gates apply-mode writes on a `confirm` elicitation behind a consent record (`api-context` § Consent gates): asking stores `{ operation, clientId, subject, target, contentHash }` in `ctx.state` under `brapi/consent/<uuid>` (600 s TTL) and returns the id as `requestState`; each call first redeems (reads and deletes) the record its `requestState` names. An answer counts only when that record matches this caller, base URL + `studyDbId`, and the SHA-256 of the rows — anything else asks again. On the matching round a declined, cancelled, or unparseable answer is terminal (`user_declined`). `force: true` skips the round. Redemption is single-use against a sequential replay only (no atomic `ctx.state.take` yet), and a multi-instance HTTP deployment needs shared storage (`filesystem`, `supabase`, `cloudflare-d1`) for the record. |
179
181
  | `ctx.enrich` | Success-path agent context. `brapi_dataframe_query` and `brapi_build_phenotype_matrix` disclose capped results with `ctx.enrich.truncated({ shown, cap, guidance })`. |
180
182
 
181
- `ctx.fail(reason, …)` is the typed thrower keyed off declared `errors[]` contracts — used by 15 tools and 1 resource today. `ctx.recoveryFor(reason)` resolves the matching contract entry's recovery hint into `data.recovery.hint` so it surfaces on the wire. `ctx.content` is unused — no tool emits media blocks outside `brapi_get_image`, which returns image bytes through its own output schema.
183
+ `ctx.fail(reason, …)` is the typed thrower keyed off declared `errors[]` contracts — used by 15 tools and 1 resource today. The framework fills the matching contract entry's recovery hint into `data.recovery.hint` on the wire, for service throws carrying `data.reason` too. `ctx.content` is unused — no tool emits media blocks outside `brapi_get_image`, which returns image bytes through its own output schema.
182
184
 
183
185
  ---
184
186
 
@@ -186,7 +188,7 @@ Handlers receive a unified `ctx` object. Currently used surface:
186
188
 
187
189
  Handlers throw — the framework catches, classifies, and formats.
188
190
 
189
- **Default for new tools: typed error contract.** Declare `errors: [{ reason, code, when, recovery, retryable? }]` on `tool()` to receive a typed `ctx.fail(reason, …)` keyed by the declared reason union. TypeScript catches `ctx.fail('typo')` at compile time, `data.reason` is auto-populated for observability, and the linter enforces conformance against the handler body. The `recovery` field is required descriptive metadata (≥ 5 words, lint-validated); to surface it on the wire, spread `...ctx.recoveryFor('reason')` into `data` or pass an explicit `{ recovery: { hint: '...' } }` when runtime context matters. Baseline codes (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`, `RequestCancelled`) bubble freely and don't need declaring. Live across the BrAPI surface today: `brapi_build_phenotype_matrix`, `brapi_connect`, `brapi_dataframe_describe`, `brapi_dataframe_export`, `brapi_dataframe_query`, `brapi_describe_filters`, `brapi_export_genotype_matrix`, `brapi_find_genotype_calls`, `brapi_germplasm_performance`, `brapi_get_germplasm`, `brapi_get_image`, `brapi_get_study`, `brapi_raw_get`, `brapi_raw_search`, `brapi_submit_observations`, plus the `brapi://variable/{observationVariableDbId}` resource.
191
+ **Default for new tools: typed error contract.** Declare `errors: [{ reason, code, when, recovery, retryable?, severity?, thrownBy? }]` on `tool()` / `resource()` to receive a typed `ctx.fail(reason, …)` keyed by the declared reason union. TypeScript catches `ctx.fail('typo')` at compile time, `data.reason` is auto-populated for observability, and the linter enforces conformance against the handler body. `recovery` is required (≥ 5 words, lint-validated) — the single source of truth for the agent's next move. The framework puts it on the wire whenever a failure carrying that `reason` arrives without a hint — a bare `ctx.fail('reason')` or a service throw with `data: { reason }` — as `data.recovery.hint`, mirrored into `content[]` text unless the message already contains it verbatim; override with an explicit `{ recovery: { hint: '...' } }` when dynamic runtime context matters. Every error envelope also carries `data.requestId`, the id the server's log records for that call carry, and `content[]` closes with `(reason … · request <id>)`. Mark an entry the service layer throws with `thrownBy: 'service'` so `error-contract-unthrown` skips it — lint-only metadata. A direct `definition.handler(...)` call in a test sees the throw site's error with no fill; assert hints through `runToolContract` (tools) or the resource factory (`tests/resources/_resource-factory.ts`). Baseline codes (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`, `RequestCancelled`) bubble freely and don't need declaring. Live across the BrAPI surface today: `brapi_build_phenotype_matrix`, `brapi_connect`, `brapi_dataframe_describe`, `brapi_dataframe_export`, `brapi_dataframe_query`, `brapi_describe_filters`, `brapi_export_genotype_matrix`, `brapi_find_genotype_calls`, `brapi_germplasm_performance`, `brapi_get_germplasm`, `brapi_get_image`, `brapi_get_study`, `brapi_raw_get`, `brapi_raw_search`, `brapi_submit_observations`, plus the `brapi://variable/{observationVariableDbId}` resource.
190
192
 
191
193
  ```ts
192
194
  errors: [
@@ -196,8 +198,7 @@ errors: [
196
198
  ],
197
199
  async handler(input, ctx) {
198
200
  const conn = registry.peek(input.alias);
199
- if (!conn) throw ctx.fail('unknown_alias', `No connection for ${input.alias}`,
200
- { ...ctx.recoveryFor('unknown_alias') });
201
+ if (!conn) throw ctx.fail('unknown_alias', `No connection for ${input.alias}`);
201
202
  // ...
202
203
  }
203
204
  ```
@@ -308,7 +309,7 @@ src/
308
309
 
309
310
  ## Skills
310
311
 
311
- Skills are modular instructions in `framework-skills/` at the project root. Read them directly when a task matches — e.g., `framework-skills/add-tool/SKILL.md` when adding a tool. Keep development skills out of root `skills/`: plugin hosts load that directory for installing agents.
312
+ Skills are modular instructions in `framework-skills/` at the project root. Read them directly when a task matches — e.g., `framework-skills/add-tool/SKILL.md` when adding a tool. `bun run list-skills` prints the full registry. The directory is deliberately not `skills/`: Claude Code and Codex auto-load a plugin's root `skills/`, so a server that ships `.claude-plugin/` or `.codex-plugin/` would hand these development skills to every agent that installs it. Keep `skills/` free for skills meant for those agents.
312
313
 
313
314
  **Agent skill directory:** Copy skills into the directory your agent discovers (Claude Code: `.claude/skills/`, Codex: `.codex/skills/`, shared: `.agents/skills/`, others: equivalent). This makes skills available as context without needing to reference `framework-skills/` paths manually. After framework updates, run the `maintenance` skill — it re-syncs the agent directory automatically (Phase B).
314
315
 
@@ -329,22 +330,21 @@ Available skills:
329
330
  | `security-pass` | Audit server for MCP-flavored security gaps: output injection, scope blast radius, input sinks, tenant isolation |
330
331
  | `code-simplifier` | Post-session cleanup against `git diff` — modernize syntax, consolidate duplication, align with the codebase |
331
332
  | `techniques` | Catalog of response/data-shaping techniques — overflow handling, payload shaping, retrieval patterns |
332
- | `devcheck` | Lint, format, typecheck, audit |
333
333
  | `polish-docs-meta` | Finalize docs, README, metadata, and agent protocol for shipping |
334
- | `git-wrapup` | Land working-tree changes as a versioned commit stack; opens a release PR when the project declares release PR mode. |
335
- | `release-pr-review` | Review an open release PR, land fixups, and keep its body current. Release PR mode only. |
336
- | `release-and-publish` | Tag + push + npm + MCP Registry + GH Release + Docker. Picks up from `git-wrapup`. |
334
+ | `git-wrapup` | Land working-tree changes as a commit stack — version bump, changelog, verify, commit by concern, release commit on top. No tag, no push to main; opens the release PR when the project declares release PR mode |
335
+ | `release-pr-review` | Review pass on an open release PR — simplifier + correctness review, fixes as ordinary commits on top of the stack, PR body kept in sync. Release PR mode only |
336
+ | `release-and-publish` | Fast-forward merge (release PR mode) + tag + push + npm + MCP Registry + GH Release + Docker. Picks up from `git-wrapup` |
337
337
  | `maintenance` | Investigate changelogs, adopt upstream changes, sync skills to agent dirs |
338
338
  | `orchestrations` | Chain task skills into a gated multi-phase pipeline — build-out, QA-fix, update-ship — when you can spawn sub-agents |
339
339
  | `report-issue-framework` | File a bug or feature request against `@cyanheads/mcp-ts-core` via `gh` CLI |
340
340
  | `report-issue-local` | File a bug or feature request against this server's own repo via `gh` CLI |
341
341
  | `api-auth` | Auth modes, scopes, JWT/OAuth |
342
- | `api-linter` | Definition lint rule reference (`format-parity`, `schema-*`, `name-*`, `server-json-*`, …) |
342
+ | `api-linter` | Definition linter rule catalog — invoked by `bun run lint:mcp` and `devcheck` |
343
343
  | `api-canvas` | DataCanvas: register tabular data, run SQL, export, plus the `spillover()` helper for big result sets — Tier 3 opt-in |
344
344
  | `api-config` | AppConfig, parseConfig, env vars |
345
345
  | `api-context` | Context interface, RequestContext, logger, state, multi-round-trip input |
346
346
  | `api-errors` | McpError, JsonRpcErrorCode, error patterns |
347
- | `api-mirror` | MirrorService: persistent SQLite-backed local mirror of a bulk upstream dataset with FTS5 — Tier 3 opt-in |
347
+ | `api-mirror` | MirrorService: persistent self-refreshing local mirror (embedded SQLite + FTS5) of a bulk upstream dataset — Tier 3 opt-in |
348
348
  | `api-services` | LLM, Speech, Graph services |
349
349
  | `api-telemetry` | OTel catalog: spans, metrics, completion logs, env config, cardinality rules |
350
350
  | `api-testing` | createMockContext, test patterns |
@@ -367,21 +367,24 @@ When you complete a skill's checklist, check the boxes and add a completion time
367
367
  | `bun run rebuild` | Clean + build |
368
368
  | `bun run clean` | Remove build artifacts |
369
369
  | `bun run devcheck` | Lint + format + typecheck + security + changelog sync |
370
- | `bun run audit:fix` | Upgrade vulnerable packages within existing ranges with `bun audit fix`; try before `bun update <name>` and `bun dedupe`. |
371
- | `bun run audit:refresh` | Delete `bun.lock`, reinstall, re-audit. Last resort after in-place fixes; re-resolves ranged dependencies, including the framework. |
370
+ | `bun run audit:fix` | `bun audit fix` — upgrade vulnerable packages to the lowest safe version within existing ranges (`--dry-run` previews, `--latest` rewrites ranges). First response when `devcheck` flags a transitive advisory; then `bun update <name>`, then `bun dedupe` |
371
+ | `bun run audit:refresh` | Delete `bun.lock` and reinstall. Last resort after `audit:fix`, `bun update <name>`, and `bun dedupe` — re-resolves every ranged dep (the framework pin included) and rewrites the lockfile as `lockfileVersion: 2` |
372
372
  | `bun run list-skills` | Print the skill index for this project (name, version, description) |
373
373
  | `bun run tree` | Generate `docs/tree.md` |
374
- | `bun run format` | Auto-fix formatting via Biome |
375
- | `bun run lint:mcp` | Validate MCP tool / resource / prompt definitions against the spec |
376
- | `bun run lint:packaging` | Verify env var alignment between `manifest.json` and `server.json` |
374
+ | `bun run format` | Auto-fix formatting (safe fixes only) |
375
+ | `bun run format:unsafe` | Also apply Biome's unsafe autofixes — review the diff; they can change behavior |
376
+ | `bun run lint:mcp` | Run the MCP definition linter standalone (rule catalog: `api-linter` skill) |
377
+ | `bun run lint:packaging` | Packaging surface checks — `server.json`/`manifest.json` env-var parity, Dockerfile build platform (run by devcheck) |
377
378
  | `bun run bundle` | Build, pack, and clean a `.mcpb` for one-click Claude Desktop install |
378
- | `bun run test` | Vitest suite |
379
+ | `bun run test` | Run tests (Vitest — use `bun run test`, not `bun test`) |
379
380
  | `bun run start` | Production mode — defers transport selection to `MCP_TRANSPORT_TYPE` (stdio default) |
380
381
  | `bun run start:stdio` | Production mode (stdio) — requires prior `bun run build` |
381
382
  | `bun run start:http` | Production mode (HTTP) — requires prior `bun run build` |
382
383
  | `bun run changelog:build` | Regenerate `CHANGELOG.md` from `changelog/<minor>.x/` |
383
384
  | `bun run changelog:check` | Verify `CHANGELOG.md` is in sync (used by devcheck) |
384
385
 
386
+ **CI is one file.** `.github/workflows/codeql.yml` (scaffolded) is the only GitHub Actions workflow: CodeQL is GitHub-owned end to end, and the file runs only while the repo's CodeQL *default setup* is turned off. Verification — `devcheck`, tests, the release gates — runs locally; don't add a workflow that re-runs it.
387
+
385
388
  ---
386
389
 
387
390
  ## Bundling
@@ -454,7 +457,7 @@ import { getMyService } from '@/services/my-domain/my-service.js';
454
457
  - [ ] Tests include at least one sparse upstream payload (fields omitted) alongside the happy path
455
458
  - [ ] Registered in the `tools` array of `createApp()` in `src/index.ts`
456
459
  - [ ] Tests use `createMockContext()` from `@cyanheads/mcp-ts-core/testing`
457
- - [ ] `.codex-plugin/plugin.json` populated — `name`, `version`, `description`, `repository`, `license` from `package.json`; `interface.displayName` = the unscoped repo name; `interface.shortDescription` from `package.json` description
458
- - [ ] `.codex-plugin/mcp.json` updated — server name key is the unscoped repo name; user-supplied variables are listed in `env_vars`, never set to empty strings in `env`
459
- - [ ] `.claude-plugin/plugin.json` populated — metadata from `package.json`; inline `mcpServers` entry keyed by the unscoped repo name; user-supplied variables declared under `userConfig` and referenced as `${user_config.<option>}`
460
+ - [ ] `.codex-plugin/plugin.json` populated — `name`, `version`, `description`, `repository`, `license` from `package.json`; `interface.displayName` = the unscoped repo name (never the npm scope — `lint:packaging` enforces this); `interface.shortDescription` from `package.json` description
461
+ - [ ] `.codex-plugin/mcp.json` updated — server name key is the unscoped repo name; every user-supplied variable (API key, contact email, instance URL) is listed in `env_vars` so Codex forwards it from the user's environment. Never write `"KEY": ""` into `env` — an empty value replaces the user's exported key and is read as unset
462
+ - [ ] `.claude-plugin/plugin.json` populated — `name`, `version`, `description`, `author`, `repository`, `license`, `keywords` from `package.json`; inline `mcpServers` entry keyed by the unscoped repo name. Every user-supplied variable is declared under `userConfig` (`type`, `title`, `description`; `sensitive: true` for keys and tokens; `required: true` or `default: ""`) and referenced from `env` as `"KEY": "${user_config.<option>}"` — mirror the `user_config` block in `manifest.json`. Never write `"KEY": ""` into `env`
460
463
  - [ ] `bun run devcheck` passes
package/CLAUDE.md CHANGED
@@ -1,10 +1,11 @@
1
1
  # Agent Protocol
2
2
 
3
3
  **Server:** brapi-mcp-server
4
- **Version:** 0.8.0
5
- **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.6`
4
+ **Version:** 0.8.1
5
+ **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.11`
6
6
  **Engines:** Bun ≥1.4.0, Node ≥24.0.0
7
- **MCP SDK:** `@modelcontextprotocol/server` ^2.0.0 (protocol revisions 2026-07-28 and 2025-*)
7
+ **MCP SDK:** `@modelcontextprotocol/server` ^2.2.0 (protocol revisions 2026-07-28 and 2025-*)
8
+ **Zod:** ^4.6.5
8
9
 
9
10
  > **Read the framework docs first:** `node_modules/@cyanheads/mcp-ts-core/CLAUDE.md` contains the full API reference — builders, Context, error codes, exports, patterns. This file covers server-specific conventions only.
10
11
 
@@ -36,6 +37,7 @@ Tailor suggestions to what's actually missing or stale — don't recite the full
36
37
  - **Use `ctx.state`** for tenant-scoped storage. Never access persistence directly.
37
38
  - **Need input the caller didn't supply?** `return ctx.requestInput(...)` and read `ctx.inputs` when the handler is re-entered. Never `await` for user input mid-handler.
38
39
  - **Secrets in env vars only** — never hardcoded.
40
+ - **Cut noise.** Add only what earns its place: no speculative generality, no guards for states the framework already prevents (Zod-validated params, classified errors), no abstraction until a third caller proves it, no option nothing sets.
39
41
  - **Close the loop on issues.** When implementing work tracked by a GitHub issue, comment on the issue with what landed and close it. Do both — a comment without a close leaves stale issues open; a close without a comment leaves no record of what shipped. The comment is for future readers — state the concrete changes, not the conversation that produced them.
40
42
 
41
43
  ---
@@ -175,10 +177,10 @@ Handlers receive a unified `ctx` object. Currently used surface:
175
177
  | `ctx.signal` | `AbortSignal` — threaded into every BrAPI HTTP call so client-side cancellation aborts the upstream request. |
176
178
  | `ctx.requestId` | Unique request ID — auto-attached to every `ctx.log` entry. |
177
179
  | `ctx.tenantId` | Tenant ID from JWT or `'default'` for stdio / HTTP+`auth=none` — outer scope on all `ctx.state` reads/writes. |
178
- | `ctx.requestInput` / `ctx.inputs` | Multi-round-trip input. `brapi_submit_observations` gates apply-mode writes on a `confirm` elicitation: it reads `ctx.inputs.view('confirm')`, returns `ctx.requestInput({ inputRequests: … })` when the answer is missing, and treats a declined, cancelled, or unparseable answer as terminal (`user_declined`). `force: true` skips the round. |
180
+ | `ctx.requestInput` / `ctx.inputs` | Multi-round-trip input. `brapi_submit_observations` gates apply-mode writes on a `confirm` elicitation behind a consent record (`api-context` § Consent gates): asking stores `{ operation, clientId, subject, target, contentHash }` in `ctx.state` under `brapi/consent/<uuid>` (600 s TTL) and returns the id as `requestState`; each call first redeems (reads and deletes) the record its `requestState` names. An answer counts only when that record matches this caller, base URL + `studyDbId`, and the SHA-256 of the rows — anything else asks again. On the matching round a declined, cancelled, or unparseable answer is terminal (`user_declined`). `force: true` skips the round. Redemption is single-use against a sequential replay only (no atomic `ctx.state.take` yet), and a multi-instance HTTP deployment needs shared storage (`filesystem`, `supabase`, `cloudflare-d1`) for the record. |
179
181
  | `ctx.enrich` | Success-path agent context. `brapi_dataframe_query` and `brapi_build_phenotype_matrix` disclose capped results with `ctx.enrich.truncated({ shown, cap, guidance })`. |
180
182
 
181
- `ctx.fail(reason, …)` is the typed thrower keyed off declared `errors[]` contracts — used by 15 tools and 1 resource today. `ctx.recoveryFor(reason)` resolves the matching contract entry's recovery hint into `data.recovery.hint` so it surfaces on the wire. `ctx.content` is unused — no tool emits media blocks outside `brapi_get_image`, which returns image bytes through its own output schema.
183
+ `ctx.fail(reason, …)` is the typed thrower keyed off declared `errors[]` contracts — used by 15 tools and 1 resource today. The framework fills the matching contract entry's recovery hint into `data.recovery.hint` on the wire, for service throws carrying `data.reason` too. `ctx.content` is unused — no tool emits media blocks outside `brapi_get_image`, which returns image bytes through its own output schema.
182
184
 
183
185
  ---
184
186
 
@@ -186,7 +188,7 @@ Handlers receive a unified `ctx` object. Currently used surface:
186
188
 
187
189
  Handlers throw — the framework catches, classifies, and formats.
188
190
 
189
- **Default for new tools: typed error contract.** Declare `errors: [{ reason, code, when, recovery, retryable? }]` on `tool()` to receive a typed `ctx.fail(reason, …)` keyed by the declared reason union. TypeScript catches `ctx.fail('typo')` at compile time, `data.reason` is auto-populated for observability, and the linter enforces conformance against the handler body. The `recovery` field is required descriptive metadata (≥ 5 words, lint-validated); to surface it on the wire, spread `...ctx.recoveryFor('reason')` into `data` or pass an explicit `{ recovery: { hint: '...' } }` when runtime context matters. Baseline codes (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`, `RequestCancelled`) bubble freely and don't need declaring. Live across the BrAPI surface today: `brapi_build_phenotype_matrix`, `brapi_connect`, `brapi_dataframe_describe`, `brapi_dataframe_export`, `brapi_dataframe_query`, `brapi_describe_filters`, `brapi_export_genotype_matrix`, `brapi_find_genotype_calls`, `brapi_germplasm_performance`, `brapi_get_germplasm`, `brapi_get_image`, `brapi_get_study`, `brapi_raw_get`, `brapi_raw_search`, `brapi_submit_observations`, plus the `brapi://variable/{observationVariableDbId}` resource.
191
+ **Default for new tools: typed error contract.** Declare `errors: [{ reason, code, when, recovery, retryable?, severity?, thrownBy? }]` on `tool()` / `resource()` to receive a typed `ctx.fail(reason, …)` keyed by the declared reason union. TypeScript catches `ctx.fail('typo')` at compile time, `data.reason` is auto-populated for observability, and the linter enforces conformance against the handler body. `recovery` is required (≥ 5 words, lint-validated) — the single source of truth for the agent's next move. The framework puts it on the wire whenever a failure carrying that `reason` arrives without a hint — a bare `ctx.fail('reason')` or a service throw with `data: { reason }` — as `data.recovery.hint`, mirrored into `content[]` text unless the message already contains it verbatim; override with an explicit `{ recovery: { hint: '...' } }` when dynamic runtime context matters. Every error envelope also carries `data.requestId`, the id the server's log records for that call carry, and `content[]` closes with `(reason … · request <id>)`. Mark an entry the service layer throws with `thrownBy: 'service'` so `error-contract-unthrown` skips it — lint-only metadata. A direct `definition.handler(...)` call in a test sees the throw site's error with no fill; assert hints through `runToolContract` (tools) or the resource factory (`tests/resources/_resource-factory.ts`). Baseline codes (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`, `RequestCancelled`) bubble freely and don't need declaring. Live across the BrAPI surface today: `brapi_build_phenotype_matrix`, `brapi_connect`, `brapi_dataframe_describe`, `brapi_dataframe_export`, `brapi_dataframe_query`, `brapi_describe_filters`, `brapi_export_genotype_matrix`, `brapi_find_genotype_calls`, `brapi_germplasm_performance`, `brapi_get_germplasm`, `brapi_get_image`, `brapi_get_study`, `brapi_raw_get`, `brapi_raw_search`, `brapi_submit_observations`, plus the `brapi://variable/{observationVariableDbId}` resource.
190
192
 
191
193
  ```ts
192
194
  errors: [
@@ -196,8 +198,7 @@ errors: [
196
198
  ],
197
199
  async handler(input, ctx) {
198
200
  const conn = registry.peek(input.alias);
199
- if (!conn) throw ctx.fail('unknown_alias', `No connection for ${input.alias}`,
200
- { ...ctx.recoveryFor('unknown_alias') });
201
+ if (!conn) throw ctx.fail('unknown_alias', `No connection for ${input.alias}`);
201
202
  // ...
202
203
  }
203
204
  ```
@@ -308,7 +309,7 @@ src/
308
309
 
309
310
  ## Skills
310
311
 
311
- Skills are modular instructions in `framework-skills/` at the project root. Read them directly when a task matches — e.g., `framework-skills/add-tool/SKILL.md` when adding a tool. Keep development skills out of root `skills/`: plugin hosts load that directory for installing agents.
312
+ Skills are modular instructions in `framework-skills/` at the project root. Read them directly when a task matches — e.g., `framework-skills/add-tool/SKILL.md` when adding a tool. `bun run list-skills` prints the full registry. The directory is deliberately not `skills/`: Claude Code and Codex auto-load a plugin's root `skills/`, so a server that ships `.claude-plugin/` or `.codex-plugin/` would hand these development skills to every agent that installs it. Keep `skills/` free for skills meant for those agents.
312
313
 
313
314
  **Agent skill directory:** Copy skills into the directory your agent discovers (Claude Code: `.claude/skills/`, Codex: `.codex/skills/`, shared: `.agents/skills/`, others: equivalent). This makes skills available as context without needing to reference `framework-skills/` paths manually. After framework updates, run the `maintenance` skill — it re-syncs the agent directory automatically (Phase B).
314
315
 
@@ -329,22 +330,21 @@ Available skills:
329
330
  | `security-pass` | Audit server for MCP-flavored security gaps: output injection, scope blast radius, input sinks, tenant isolation |
330
331
  | `code-simplifier` | Post-session cleanup against `git diff` — modernize syntax, consolidate duplication, align with the codebase |
331
332
  | `techniques` | Catalog of response/data-shaping techniques — overflow handling, payload shaping, retrieval patterns |
332
- | `devcheck` | Lint, format, typecheck, audit |
333
333
  | `polish-docs-meta` | Finalize docs, README, metadata, and agent protocol for shipping |
334
- | `git-wrapup` | Land working-tree changes as a versioned commit stack; opens a release PR when the project declares release PR mode. |
335
- | `release-pr-review` | Review an open release PR, land fixups, and keep its body current. Release PR mode only. |
336
- | `release-and-publish` | Tag + push + npm + MCP Registry + GH Release + Docker. Picks up from `git-wrapup`. |
334
+ | `git-wrapup` | Land working-tree changes as a commit stack — version bump, changelog, verify, commit by concern, release commit on top. No tag, no push to main; opens the release PR when the project declares release PR mode |
335
+ | `release-pr-review` | Review pass on an open release PR — simplifier + correctness review, fixes as ordinary commits on top of the stack, PR body kept in sync. Release PR mode only |
336
+ | `release-and-publish` | Fast-forward merge (release PR mode) + tag + push + npm + MCP Registry + GH Release + Docker. Picks up from `git-wrapup` |
337
337
  | `maintenance` | Investigate changelogs, adopt upstream changes, sync skills to agent dirs |
338
338
  | `orchestrations` | Chain task skills into a gated multi-phase pipeline — build-out, QA-fix, update-ship — when you can spawn sub-agents |
339
339
  | `report-issue-framework` | File a bug or feature request against `@cyanheads/mcp-ts-core` via `gh` CLI |
340
340
  | `report-issue-local` | File a bug or feature request against this server's own repo via `gh` CLI |
341
341
  | `api-auth` | Auth modes, scopes, JWT/OAuth |
342
- | `api-linter` | Definition lint rule reference (`format-parity`, `schema-*`, `name-*`, `server-json-*`, …) |
342
+ | `api-linter` | Definition linter rule catalog — invoked by `bun run lint:mcp` and `devcheck` |
343
343
  | `api-canvas` | DataCanvas: register tabular data, run SQL, export, plus the `spillover()` helper for big result sets — Tier 3 opt-in |
344
344
  | `api-config` | AppConfig, parseConfig, env vars |
345
345
  | `api-context` | Context interface, RequestContext, logger, state, multi-round-trip input |
346
346
  | `api-errors` | McpError, JsonRpcErrorCode, error patterns |
347
- | `api-mirror` | MirrorService: persistent SQLite-backed local mirror of a bulk upstream dataset with FTS5 — Tier 3 opt-in |
347
+ | `api-mirror` | MirrorService: persistent self-refreshing local mirror (embedded SQLite + FTS5) of a bulk upstream dataset — Tier 3 opt-in |
348
348
  | `api-services` | LLM, Speech, Graph services |
349
349
  | `api-telemetry` | OTel catalog: spans, metrics, completion logs, env config, cardinality rules |
350
350
  | `api-testing` | createMockContext, test patterns |
@@ -367,21 +367,24 @@ When you complete a skill's checklist, check the boxes and add a completion time
367
367
  | `bun run rebuild` | Clean + build |
368
368
  | `bun run clean` | Remove build artifacts |
369
369
  | `bun run devcheck` | Lint + format + typecheck + security + changelog sync |
370
- | `bun run audit:fix` | Upgrade vulnerable packages within existing ranges with `bun audit fix`; try before `bun update <name>` and `bun dedupe`. |
371
- | `bun run audit:refresh` | Delete `bun.lock`, reinstall, re-audit. Last resort after in-place fixes; re-resolves ranged dependencies, including the framework. |
370
+ | `bun run audit:fix` | `bun audit fix` — upgrade vulnerable packages to the lowest safe version within existing ranges (`--dry-run` previews, `--latest` rewrites ranges). First response when `devcheck` flags a transitive advisory; then `bun update <name>`, then `bun dedupe` |
371
+ | `bun run audit:refresh` | Delete `bun.lock` and reinstall. Last resort after `audit:fix`, `bun update <name>`, and `bun dedupe` — re-resolves every ranged dep (the framework pin included) and rewrites the lockfile as `lockfileVersion: 2` |
372
372
  | `bun run list-skills` | Print the skill index for this project (name, version, description) |
373
373
  | `bun run tree` | Generate `docs/tree.md` |
374
- | `bun run format` | Auto-fix formatting via Biome |
375
- | `bun run lint:mcp` | Validate MCP tool / resource / prompt definitions against the spec |
376
- | `bun run lint:packaging` | Verify env var alignment between `manifest.json` and `server.json` |
374
+ | `bun run format` | Auto-fix formatting (safe fixes only) |
375
+ | `bun run format:unsafe` | Also apply Biome's unsafe autofixes — review the diff; they can change behavior |
376
+ | `bun run lint:mcp` | Run the MCP definition linter standalone (rule catalog: `api-linter` skill) |
377
+ | `bun run lint:packaging` | Packaging surface checks — `server.json`/`manifest.json` env-var parity, Dockerfile build platform (run by devcheck) |
377
378
  | `bun run bundle` | Build, pack, and clean a `.mcpb` for one-click Claude Desktop install |
378
- | `bun run test` | Vitest suite |
379
+ | `bun run test` | Run tests (Vitest — use `bun run test`, not `bun test`) |
379
380
  | `bun run start` | Production mode — defers transport selection to `MCP_TRANSPORT_TYPE` (stdio default) |
380
381
  | `bun run start:stdio` | Production mode (stdio) — requires prior `bun run build` |
381
382
  | `bun run start:http` | Production mode (HTTP) — requires prior `bun run build` |
382
383
  | `bun run changelog:build` | Regenerate `CHANGELOG.md` from `changelog/<minor>.x/` |
383
384
  | `bun run changelog:check` | Verify `CHANGELOG.md` is in sync (used by devcheck) |
384
385
 
386
+ **CI is one file.** `.github/workflows/codeql.yml` (scaffolded) is the only GitHub Actions workflow: CodeQL is GitHub-owned end to end, and the file runs only while the repo's CodeQL *default setup* is turned off. Verification — `devcheck`, tests, the release gates — runs locally; don't add a workflow that re-runs it.
387
+
385
388
  ---
386
389
 
387
390
  ## Bundling
@@ -454,7 +457,7 @@ import { getMyService } from '@/services/my-domain/my-service.js';
454
457
  - [ ] Tests include at least one sparse upstream payload (fields omitted) alongside the happy path
455
458
  - [ ] Registered in the `tools` array of `createApp()` in `src/index.ts`
456
459
  - [ ] Tests use `createMockContext()` from `@cyanheads/mcp-ts-core/testing`
457
- - [ ] `.codex-plugin/plugin.json` populated — `name`, `version`, `description`, `repository`, `license` from `package.json`; `interface.displayName` = the unscoped repo name; `interface.shortDescription` from `package.json` description
458
- - [ ] `.codex-plugin/mcp.json` updated — server name key is the unscoped repo name; user-supplied variables are listed in `env_vars`, never set to empty strings in `env`
459
- - [ ] `.claude-plugin/plugin.json` populated — metadata from `package.json`; inline `mcpServers` entry keyed by the unscoped repo name; user-supplied variables declared under `userConfig` and referenced as `${user_config.<option>}`
460
+ - [ ] `.codex-plugin/plugin.json` populated — `name`, `version`, `description`, `repository`, `license` from `package.json`; `interface.displayName` = the unscoped repo name (never the npm scope — `lint:packaging` enforces this); `interface.shortDescription` from `package.json` description
461
+ - [ ] `.codex-plugin/mcp.json` updated — server name key is the unscoped repo name; every user-supplied variable (API key, contact email, instance URL) is listed in `env_vars` so Codex forwards it from the user's environment. Never write `"KEY": ""` into `env` — an empty value replaces the user's exported key and is read as unset
462
+ - [ ] `.claude-plugin/plugin.json` populated — `name`, `version`, `description`, `author`, `repository`, `license`, `keywords` from `package.json`; inline `mcpServers` entry keyed by the unscoped repo name. Every user-supplied variable is declared under `userConfig` (`type`, `title`, `description`; `sensitive: true` for keys and tokens; `required: true` or `default: ""`) and referenced from `env` as `"KEY": "${user_config.<option>}"` — mirror the `user_config` block in `manifest.json`. Never write `"KEY": ""` into `env`
460
463
  - [ ] `bun run devcheck` passes
package/Dockerfile CHANGED
@@ -4,15 +4,17 @@
4
4
  # This stage installs all dependencies (including dev), builds the TypeScript
5
5
  # source code into JavaScript, and prepares the production assets.
6
6
  #
7
- # Pinned to BUILDPLATFORM, not the target platform. `bun run build` emits
8
- # platform-independent JavaScript and only `dist/` is copied forward, so this
9
- # stage has no reason to run under emulation — and under QEMU it does not run
10
- # at all: Bun's JavaScriptCore aborts with a spurious MemoryExhaustion
11
- # assertion (~21 MB peak), so a cross-arch build of this stage fails outright
12
- # on an arm64 host. Building natively also removes emulation from the slowest
13
- # stage of a multi-arch build.
7
+ # Pinned to $BUILDPLATFORM rather than the target platform: `bun run build` emits
8
+ # JavaScript, and only `dist/` crosses into the production stage. Built for the
9
+ # target instead, the non-native leg of a `--platform linux/amd64,linux/arm64`
10
+ # build runs under QEMU, where bun >= 1.4 aborts with a JavaScriptCore allocator
11
+ # assertion and fails the multi-arch push.
12
+ #
13
+ # The constraint this assumes: the build stage produces platform-independent
14
+ # output. A stage that compiles a native addon needs the target-arch toolchain
15
+ # and cannot cross-compile this way — drop the flag there.
14
16
  # ==============================================================================
15
- FROM --platform=$BUILDPLATFORM oven/bun:1.4.0 AS build
17
+ FROM --platform=$BUILDPLATFORM oven/bun:1.4.2 AS build
16
18
 
17
19
  WORKDIR /usr/src/app
18
20
 
@@ -35,69 +37,105 @@ RUN bun run build
35
37
 
36
38
 
37
39
  # ==============================================================================
38
- # Production Stage
40
+ # Production Dependencies Stage
39
41
  #
40
- # This stage creates a minimal, optimized, and secure image for running the
41
- # application. It uses a slim base image and only includes production
42
- # dependencies and build artifacts.
42
+ # Installs the production dependency tree for the target platform. Every step
43
+ # here can run JavaScript — bunfig.toml's security scanner runs as a Bun
44
+ # program, and so do the OTel and musl-prune scripts — so the stage runs on
45
+ # $BUILDPLATFORM and cross-installs with `--os`/`--cpu`, which pick each
46
+ # platform-specific optional dependency (DuckDB's native bindings among them)
47
+ # for the target. Only `node_modules` leaves this stage.
48
+ #
49
+ # A clean image rather than `FROM build`: the build stage's node_modules holds
50
+ # devDependencies.
43
51
  # ==============================================================================
44
- FROM oven/bun:1.4.0-slim AS production
52
+ FROM --platform=$BUILDPLATFORM oven/bun:1.4.2 AS deps
45
53
 
46
54
  WORKDIR /usr/src/app
47
55
 
48
- # Set the environment to production for performance and to ensure only
49
- # production dependencies are installed.
50
- ENV NODE_ENV=production
51
-
52
- # OCI image metadata (https://github.com/opencontainers/image-spec/blob/main/annotations.md)
53
- ARG APP_VERSION=dev
54
- LABEL org.opencontainers.image.title="@cyanheads/brapi-mcp-server"
55
- LABEL org.opencontainers.image.description="BrAPI v2.1 MCP server — studies, germplasm, observations, genotypes, images, and pedigrees across Breedbase, T3, Sweetpotatobase, and any BrAPI-compliant server."
56
- LABEL org.opencontainers.image.source="https://github.com/cyanheads/brapi-mcp-server"
57
- LABEL org.opencontainers.image.licenses="Apache-2.0"
58
- LABEL org.opencontainers.image.version="${APP_VERSION}"
59
-
60
- # Copy dependency manifests
61
- COPY package.json bun.lock ./
56
+ # Copy dependency manifests. `bunfig.toml` rides along so every install below
57
+ # passes its release-age gate and security scanner, as a local install does.
58
+ COPY package.json bun.lock bunfig.toml ./
59
+
60
+ # The scanner bunfig.toml names is a devDependency, and Bun installs a missing
61
+ # scanner through the same production-filtered install, which omits it and
62
+ # aborts. Seed it from the build stage's full install instead. Remove this line,
63
+ # and the `rm` at the end of this stage, if bunfig.toml stops naming a scanner.
64
+ COPY --from=build /usr/src/app/node_modules/@socketsecurity/bun-security-scanner ./node_modules/@socketsecurity/bun-security-scanner
65
+
66
+ # Docker names the target architecture `amd64`/`arm64`; Bun's `--cpu` takes
67
+ # `x64`/`arm64`. Mapped once here, read by both installs below. `oven/bun`
68
+ # publishes only these two architectures, so any other target fails here.
69
+ ARG TARGETOS
70
+ ARG TARGETARCH
71
+ RUN case "$TARGETARCH" in \
72
+ amd64) echo x64 ;; \
73
+ arm64) echo arm64 ;; \
74
+ *) echo "Unsupported TARGETARCH '$TARGETARCH': expected amd64 or arm64" >&2; exit 1 ;; \
75
+ esac > .bun-cpu
62
76
 
63
77
  # Install only production dependencies, ignoring any lifecycle scripts (like 'prepare')
64
78
  # that are not needed in the final production image.
65
79
  # `--omit=peer` drops the framework's optional peer tiers (test runner, service
66
80
  # SDKs, parsers) that Bun would otherwise auto-install. Anything this server
67
- # actually imports belongs in its own `dependencies` — @duckdb/node-api among
68
- # them — so nothing needed at runtime is lost. The two `bun add` steps below
69
- # carry the same flag; without it, they re-resolve the graph and pull every
70
- # optional peer back in.
81
+ # actually imports belongs in its own `dependencies` — @duckdb/node-api, which
82
+ # backs the mandatory dataframe (canvas) surface, among them — so nothing needed
83
+ # at runtime is lost.
71
84
  RUN --mount=type=cache,target=/root/.bun/install/cache \
72
- bun install --production --omit=peer --frozen-lockfile --ignore-scripts
85
+ bun install --production --omit=peer --frozen-lockfile --ignore-scripts \
86
+ --os="$TARGETOS" --cpu="$(cat .bun-cpu)"
73
87
 
74
88
  # Conditionally install OpenTelemetry optional peer dependencies (Tier 3).
75
- # These are not bundled by default to keep the base image lean. Enable at build time
76
- # with: docker build --build-arg OTEL_ENABLED=true
89
+ # Installed by default. Omit them for a leaner image at build time
90
+ # with: docker build --build-arg OTEL_ENABLED=false
91
+ # The script reads the list and each range from the installed framework's
92
+ # `peerDependencies` and passes the target flags on to its `bun install`.
93
+ COPY scripts/install-otel.ts ./scripts/
77
94
  ARG OTEL_ENABLED=true
78
95
  RUN --mount=type=cache,target=/root/.bun/install/cache \
79
96
  if [ "$OTEL_ENABLED" = "true" ]; then \
80
- bun add --omit=dev --omit=peer --ignore-scripts @hono/otel \
81
- @opentelemetry/instrumentation-http \
82
- @opentelemetry/exporter-metrics-otlp-http \
83
- @opentelemetry/exporter-trace-otlp-http \
84
- @opentelemetry/instrumentation-pino \
85
- @opentelemetry/resources \
86
- @opentelemetry/sdk-metrics \
87
- @opentelemetry/sdk-node \
88
- @opentelemetry/sdk-trace-node \
89
- @opentelemetry/semantic-conventions; \
97
+ bun scripts/install-otel.ts --os="$TARGETOS" --cpu="$(cat .bun-cpu)"; \
90
98
  fi
91
99
 
92
- # Conditionally install the DuckDB optional peer dependency for the dataframe
93
- # (canvas) surface. Default-on so the published image works out of the box with
94
- # CANVAS_PROVIDER_TYPE=duckdb. Disable for size-sensitive self-builds with:
95
- # docker build --build-arg CANVAS_ENABLED=false
96
- ARG CANVAS_ENABLED=true
97
- RUN --mount=type=cache,target=/root/.bun/install/cache \
98
- if [ "$CANVAS_ENABLED" = "true" ]; then \
99
- bun add --omit=dev --omit=peer @duckdb/node-api; \
100
- fi
100
+ # `--os`/`--cpu` have no libc counterpart, so a native dependency published in
101
+ # glibc and musl variants (DuckDB's bindings, for one) installs both. The
102
+ # runtime image is Debian (glibc) and never loads the musl copy; the script
103
+ # deletes every package whose own `libc` admits only musl. It follows every
104
+ # install, since a later `bun install` restores what it removes.
105
+ COPY scripts/prune-musl-packages.ts ./scripts/
106
+ RUN bun scripts/prune-musl-packages.ts
107
+
108
+ # The seeded scanner served only the installs above; keep it out of the image.
109
+ RUN rm -rf node_modules/@socketsecurity/bun-security-scanner
110
+
111
+
112
+ # ==============================================================================
113
+ # Production Stage
114
+ #
115
+ # This stage creates a minimal, optimized, and secure image for running the
116
+ # application. It uses a slim base image and only includes production
117
+ # dependencies and build artifacts. Its only Bun invocations are HEALTHCHECK
118
+ # and CMD, which run on the real target at container start.
119
+ # ==============================================================================
120
+ FROM oven/bun:1.4.2-slim AS production
121
+
122
+ WORKDIR /usr/src/app
123
+
124
+ # Set the environment to production for performance.
125
+ ENV NODE_ENV=production
126
+
127
+ # OCI image metadata (https://github.com/opencontainers/image-spec/blob/main/annotations.md)
128
+ ARG APP_VERSION=dev
129
+ LABEL org.opencontainers.image.title="@cyanheads/brapi-mcp-server"
130
+ LABEL org.opencontainers.image.description="BrAPI v2.1 MCP server — studies, germplasm, observations, genotypes, images, and pedigrees across Breedbase, T3, Sweetpotatobase, and any BrAPI-compliant server."
131
+ LABEL org.opencontainers.image.source="https://github.com/cyanheads/brapi-mcp-server"
132
+ LABEL org.opencontainers.image.licenses="Apache-2.0"
133
+ LABEL org.opencontainers.image.version="${APP_VERSION}"
134
+
135
+ # The manifest comes from the build context: the deps stage's copy was rewritten
136
+ # by the OTel install, and the runtime reads only its name, version, and type.
137
+ COPY package.json ./
138
+ COPY --from=deps /usr/src/app/node_modules ./node_modules
101
139
 
102
140
  # Copy the compiled application code from the build stage
103
141
  COPY --from=build /usr/src/app/dist ./dist
@@ -117,13 +155,14 @@ ARG PORT
117
155
 
118
156
  # Set runtime environment variables
119
157
  # Note: PORT is an automatic variable in many cloud environments (e.g., Cloud Run)
158
+ # MCP_SESSION_MODE stays stateful: the server declares `sessionMode.require:
159
+ # 'stateful'` and refuses to start over HTTP in stateless mode.
120
160
  ENV MCP_HTTP_PORT=${PORT:-3010}
121
161
  ENV MCP_HTTP_HOST="0.0.0.0"
122
162
  ENV MCP_TRANSPORT_TYPE="http"
123
163
  ENV MCP_SESSION_MODE="stateful"
124
164
  ENV MCP_LOG_LEVEL="info"
125
165
  ENV LOGS_DIR="/var/log/brapi-mcp-server"
126
- ENV MCP_FORCE_CONSOLE_LOGGING="true"
127
166
 
128
167
  # Expose the port the server listens on
129
168
  EXPOSE ${MCP_HTTP_PORT}
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  <div align="center">
9
9
 
10
- [![Version](https://img.shields.io/badge/Version-0.8.0-blue.svg?style=flat-square)](./CHANGELOG.md) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat-square)](./LICENSE) [![Docker](https://img.shields.io/badge/Docker-ghcr.io-2496ED?style=flat-square&logo=docker&logoColor=white)](https://github.com/cyanheads/brapi-mcp-server/pkgs/container/brapi-mcp-server) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^2.0.0-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![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) [![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)
10
+ [![Version](https://img.shields.io/badge/Version-0.8.1-blue.svg?style=flat-square)](./CHANGELOG.md) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat-square)](./LICENSE) [![Docker](https://img.shields.io/badge/Docker-ghcr.io-2496ED?style=flat-square&logo=docker&logoColor=white)](https://github.com/cyanheads/brapi-mcp-server/pkgs/container/brapi-mcp-server) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^2.2.0-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![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) [![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.2-blueviolet.svg?style=flat-square)](https://bun.sh/) [![Status](https://img.shields.io/badge/Status-Beta-yellow.svg?style=flat-square)](./CHANGELOG.md)
11
11
 
12
12
  </div>
13
13
 
@@ -246,7 +246,7 @@ Every resource reads the `default` connection and mirrors a tool; tool-only clie
246
246
  ### `brapi_submit_observations` <sub>tool</sub>
247
247
 
248
248
  - `studyDbId` plus 1–5,000 `observations`; a row with `observationDbId` updates via `PUT`, one without creates via `POST`, and nothing is deleted
249
- - `mode: "preview"` (default) returns `valid`, `invalid`, `routing` counts, and `perRowWarnings` without writing; `mode: "apply"` asks the caller to confirm (`force: true` skips it), writes, and returns `posted`, `updated`, and `studyObservationCount`; failures are `observations_unsupported`, `study_not_found`, `post_unsupported`, `put_unsupported`, and `user_declined`
249
+ - `mode: "preview"` (default) returns `valid`, `invalid`, `routing` counts, and `perRowWarnings` without writing; `mode: "apply"` asks the caller to confirm (`force: true` skips 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 returns `posted`, `updated`, and `studyObservationCount`; failures are `observations_unsupported`, `study_not_found`, `post_unsupported`, `put_unsupported`, and `user_declined`
250
250
  - Registered only when `BRAPI_ENABLE_WRITES=true`; requires the `brapi:write:observations` scope
251
251
 
252
252
  ---
@@ -492,11 +492,14 @@ Every variable is optional.
492
492
  | `BRAPI_GENOTYPE_MATRIX_MAX_COLUMNS` | Variant-column ceiling per `brapi_export_genotype_matrix` matrix. Max `500000`. | `10000` |
493
493
  | `MCP_TRANSPORT_TYPE` | Transport: `stdio` or `http`. | `stdio` |
494
494
  | `MCP_HTTP_PORT` | HTTP server port. | `3010` |
495
- | `MCP_SESSION_MODE` | HTTP session mode. This server requires `stateful` (apply-mode writes confirm over the session, and isolation keys off it); an HTTP start fails if it resolves to `stateless`. | `stateful` |
495
+ | `MCP_SESSION_MODE` | HTTP session mode. This server requires `stateful` (apply-mode writes confirm over the session, and isolation keys off it); an HTTP start fails if it resolves to `stateless`. | `auto` (resolves `stateful`) |
496
496
  | `MCP_AUTH_MODE` | Authentication: `none`, `jwt`, or `oauth`. | `none` |
497
+ | `MCP_REQUEST_STATE_KEY` | Secret of at least 32 bytes, the same on every instance, that seals the `requestState` a confirmation round returns; any other state is refused before the handler runs. Recommended with `BRAPI_ENABLE_WRITES`. | — |
497
498
  | `MCP_LOG_LEVEL` | Log level (`debug`, `info`, `warning`, `error`, etc.). | `info` |
499
+ | `LOG_TOOL_FAILURE_PAYLOADS` | Log each failed tool call's arguments and result (key-name redaction only). | `false` |
498
500
  | `STORAGE_PROVIDER_TYPE` | Storage backend: `in-memory`, `filesystem`, `supabase`, `cloudflare-kv/r2/d1`. | `in-memory` |
499
501
  | `OTEL_ENABLED` | Enable [OpenTelemetry](https://github.com/cyanheads/mcp-ts-core/tree/main/docs/telemetry). | `false` |
502
+ | `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` | Opt-in OTLP log export endpoint; the base `OTEL_EXPORTER_OTLP_ENDPOINT` never enables it. | — |
500
503
 
501
504
  See [`.env.example`](./.env.example) for the full list of optional overrides.
502
505
 
@@ -0,0 +1,38 @@
1
+ ---
2
+ summary: "Apply-mode brapi_submit_observations writes now proceed only on the round that redeems a single-use consent record of the prompt, so a pre-supplied or replayed confirmation asks again. Adopts @cyanheads/mcp-ts-core ^0.13.11."
3
+ breaking: false
4
+ security: true
5
+ ---
6
+
7
+ # 0.8.1 — 2026-10-04
8
+
9
+ ## Added
10
+
11
+ - **`MCP_REQUEST_STATE_KEY`** documented in `.env.example` and the README — an opt-in secret (≥ 32 bytes, identical on every instance) that seals the `requestState` a confirmation round returns; recommended with `BRAPI_ENABLE_WRITES`.
12
+ - **`LOG_TOOL_FAILURE_PAYLOADS`** and **`OTEL_EXPORTER_OTLP_LOGS_ENDPOINT`** documented in `.env.example` and the README (opt-in failed-call payload logging and OTLP log export).
13
+
14
+ ## Changed
15
+
16
+ - **Recovery hints** — redundant `ctx.recoveryFor` forwards are gone; the framework (0.13.10) fills a declared `recovery` hint from the tool's `errors[]` entry, so the hint text callers see is unchanged.
17
+ - **`sessionMode`** is declared as `{ require: 'stateful' }` — no behavior change: an unset `MCP_SESSION_MODE` still resolves stateful on HTTP, and `stateless` still fails startup.
18
+ - **Framework changes reaching callers and operators:**
19
+ - 0.13.7: server stack traces no longer reach clients in error `data`; with `OTEL_ENABLED=true`, `OTEL_EXPORTER_OTLP_ENDPOINT` alone exports traces and metrics, and a deployment with no endpoint set exports nothing.
20
+ - 0.13.8: request context no longer reaches client error `data`; a result that breaks a tool's output schema fails as `-32603`.
21
+ - 0.13.9: a JSON-stringified object, or an integer sent for a string field, is repaired before validation; argument rejections name alias rewrites; canvas (DuckDB) scratch files live in a private per-process directory; MCP SDK 2.1 transport changes.
22
+ - 0.13.10: `ctx.inputs` keeps only responses the client's declared capabilities cover; tool, resource, and prompt error envelopes carry `data.requestId`, and a tool error's `content[]` closes with `request <id>`.
23
+ - 0.13.11: `ctx.log` notifications honor `MCP_LOG_LEVEL` and mask sensitive fields.
24
+ - **Dockerfile** — a new `deps` stage runs on `$BUILDPLATFORM` and cross-installs production dependencies with `--os`/`--cpu`, so multi-arch builds no longer run Bun under emulation (0.13.10); musl-only packages are pruned via `scripts/prune-musl-packages.ts` (0.13.11), and OTel peers install via `scripts/install-otel.ts`. Base images move to Bun 1.4.2. The `CANVAS_ENABLED` build arg is gone — `@duckdb/node-api` is a regular dependency, so `--build-arg CANVAS_ENABLED=false` never produced a smaller image — as is `ENV MCP_FORCE_CONSOLE_LOGGING`, which nothing read (0.13.8).
25
+ - **`server.json`** npm entries drop the `run start:*` arguments (`runtimeHint` `npx`), and the HTTP entry fixes `MCP_TRANSPORT_TYPE` to `http` (0.13.11).
26
+ - **Env-file ignore rules** — `.gitignore` and `.dockerignore` ignore every `.env*` file except committed templates; the Docker context also drops MCP registry token files.
27
+ - **CodeQL workflow** runs one matrix leg per language and uploads under `/language:<language>`.
28
+
29
+ ## Security
30
+
31
+ - **`brapi_submit_observations` consent gate** — asking for confirmation stores a single-use `ctx.state` record (operation, caller client/subject, target = base URL + `studyDbId`, SHA-256 of the rows; 600 s TTL) under a random id returned as `requestState`, and an apply-mode write proceeds only on the round that redeems a matching record with an accepted confirm. A pre-supplied answer with no record, a mismatched record, or a replayed id asks again; `force: true` and preview mode are unchanged. Redemption is not atomic under concurrent retries ([cyanheads/mcp-ts-core#593](https://github.com/cyanheads/mcp-ts-core/issues/593)), and a multi-instance HTTP deployment needs shared storage for the record.
32
+
33
+ ## Dependencies
34
+
35
+ - `@cyanheads/mcp-ts-core` ^0.13.6 → ^0.13.11 (MCP SDK `@modelcontextprotocol/server` ^2.2.0)
36
+ - `@duckdb/node-api` ^1.5.5-r.5 → ^1.5.6-r.1
37
+ - Dev: `@biomejs/biome` ^2.5.14 → ^2.5.15, `@socketsecurity/bun-security-scanner` ^1.1.2 → ^1.1.3, `@types/node` ^26.6.2 → ^26.6.3, `ignore` ^7.0.9 → ^7.0.11, `tsc-alias` ^1.9.5 → ^1.9.7, `vitest` ^5.0.1 → ^5.0.3
38
+ - `packageManager` bun@1.4.0 → bun@1.4.2
package/dist/index.js CHANGED
@@ -99,11 +99,12 @@ await createApp({
99
99
  title: 'brapi-mcp-server',
100
100
  // `brapi_submit_observations` gates apply-mode writes on a `ctx.requestInput`
101
101
  // confirmation round trip, which a 2025-era HTTP client can only answer on a
102
- // durable session — and per-session isolation keys off `ctx.sessionId`. Declare
103
- // the posture here rather than leaving it to the env: `require` fails startup
104
- // with a ConfigurationError if MCP_SESSION_MODE resolves HTTP to `stateless`,
105
- // instead of silently degrading the write path. Never refuses a stdio start.
106
- sessionMode: { default: 'stateful', require: 'stateful' },
102
+ // durable session — and per-session isolation keys off `ctx.sessionId`. `require`
103
+ // fails startup with a ConfigurationError if MCP_SESSION_MODE resolves HTTP to
104
+ // `stateless`, instead of silently degrading the write path; an unset
105
+ // MCP_SESSION_MODE reads as `auto`, which resolves stateful. Never refuses a
106
+ // stdio start.
107
+ sessionMode: { require: 'stateful' },
107
108
  tools,
108
109
  resources: [
109
110
  brapiServerInfoResource,