@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.
- package/AGENTS.md +27 -24
- package/CLAUDE.md +27 -24
- package/Dockerfile +93 -54
- package/README.md +6 -3
- package/changelog/0.8.x/0.8.1.md +38 -0
- package/dist/index.js +6 -5
- package/dist/index.js.map +1 -1
- package/dist/mcp-server/resources/definitions/brapi-variable.resource.d.ts.map +1 -1
- package/dist/mcp-server/resources/definitions/brapi-variable.resource.js +2 -10
- package/dist/mcp-server/resources/definitions/brapi-variable.resource.js.map +1 -1
- package/dist/mcp-server/tools/definitions/brapi-build-phenotype-matrix.tool.js +1 -1
- package/dist/mcp-server/tools/definitions/brapi-build-phenotype-matrix.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/brapi-connect.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/brapi-connect.tool.js +1 -1
- package/dist/mcp-server/tools/definitions/brapi-connect.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/brapi-dataframe-describe.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/brapi-dataframe-describe.tool.js +1 -3
- package/dist/mcp-server/tools/definitions/brapi-dataframe-describe.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/brapi-dataframe-export.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/brapi-dataframe-export.tool.js +4 -10
- package/dist/mcp-server/tools/definitions/brapi-dataframe-export.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/brapi-dataframe-query.tool.js +1 -1
- package/dist/mcp-server/tools/definitions/brapi-dataframe-query.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/brapi-describe-filters.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/brapi-describe-filters.tool.js +1 -5
- package/dist/mcp-server/tools/definitions/brapi-describe-filters.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/brapi-export-genotype-matrix.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/brapi-export-genotype-matrix.tool.js +2 -2
- package/dist/mcp-server/tools/definitions/brapi-export-genotype-matrix.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/brapi-find-genotype-calls.tool.js +2 -2
- package/dist/mcp-server/tools/definitions/brapi-find-genotype-calls.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/brapi-germplasm-performance.tool.js +1 -5
- package/dist/mcp-server/tools/definitions/brapi-germplasm-performance.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/brapi-get-germplasm.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/brapi-get-germplasm.tool.js +2 -10
- package/dist/mcp-server/tools/definitions/brapi-get-germplasm.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/brapi-get-image.tool.js +1 -1
- package/dist/mcp-server/tools/definitions/brapi-get-image.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/brapi-get-study.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/brapi-get-study.tool.js +2 -10
- package/dist/mcp-server/tools/definitions/brapi-get-study.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/brapi-raw-get.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/brapi-raw-get.tool.js +0 -1
- package/dist/mcp-server/tools/definitions/brapi-raw-get.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/brapi-raw-search.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/brapi-raw-search.tool.js +1 -5
- package/dist/mcp-server/tools/definitions/brapi-raw-search.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/brapi-submit-observations.tool.d.ts +5 -0
- package/dist/mcp-server/tools/definitions/brapi-submit-observations.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/brapi-submit-observations.tool.js +55 -10
- package/dist/mcp-server/tools/definitions/brapi-submit-observations.tool.js.map +1 -1
- package/dist/mcp-server/tools/shared/find-helpers.d.ts +5 -5
- package/dist/mcp-server/tools/shared/find-helpers.d.ts.map +1 -1
- package/dist/mcp-server/tools/shared/find-helpers.js +7 -11
- package/dist/mcp-server/tools/shared/find-helpers.js.map +1 -1
- package/dist/services/capability-registry/capability-registry.d.ts.map +1 -1
- package/dist/services/capability-registry/capability-registry.js +6 -6
- package/dist/services/capability-registry/capability-registry.js.map +1 -1
- package/dist/services/server-registry/server-registry.d.ts.map +1 -1
- package/dist/services/server-registry/server-registry.js +0 -3
- package/dist/services/server-registry/server-registry.js.map +1 -1
- package/manifest.json +1 -1
- package/package.json +11 -10
- 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.
|
|
5
|
-
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.
|
|
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.
|
|
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:
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
335
|
-
| `release-pr-review` | Review an open release PR
|
|
336
|
-
| `release-and-publish` |
|
|
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
|
|
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
|
|
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` |
|
|
371
|
-
| `bun run audit:refresh` | Delete `bun.lock
|
|
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
|
|
375
|
-
| `bun run
|
|
376
|
-
| `bun run lint:
|
|
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
|
|
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
|
|
459
|
-
- [ ] `.claude-plugin/plugin.json` populated —
|
|
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.
|
|
5
|
-
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.
|
|
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.
|
|
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:
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
335
|
-
| `release-pr-review` | Review an open release PR
|
|
336
|
-
| `release-and-publish` |
|
|
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
|
|
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
|
|
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` |
|
|
371
|
-
| `bun run audit:refresh` | Delete `bun.lock
|
|
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
|
|
375
|
-
| `bun run
|
|
376
|
-
| `bun run lint:
|
|
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
|
|
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
|
|
459
|
-
- [ ] `.claude-plugin/plugin.json` populated —
|
|
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
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
# assertion
|
|
12
|
-
#
|
|
13
|
-
#
|
|
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.
|
|
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
|
-
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
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.
|
|
52
|
+
FROM --platform=$BUILDPLATFORM oven/bun:1.4.2 AS deps
|
|
45
53
|
|
|
46
54
|
WORKDIR /usr/src/app
|
|
47
55
|
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
#
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
#
|
|
61
|
-
|
|
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
|
|
68
|
-
#
|
|
69
|
-
#
|
|
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
|
-
#
|
|
76
|
-
# with: docker build --build-arg OTEL_ENABLED=
|
|
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
|
|
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
|
-
#
|
|
93
|
-
#
|
|
94
|
-
#
|
|
95
|
-
#
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
[](./CHANGELOG.md) [](./LICENSE) [](https://github.com/cyanheads/brapi-mcp-server/pkgs/container/brapi-mcp-server) [](https://modelcontextprotocol.io/) [](https://www.npmjs.com/package/@cyanheads/brapi-mcp-server) [](https://www.typescriptlang.org/) [](https://bun.sh/) [](./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)
|
|
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`.
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
|
|
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,
|