# Nimbus BCI

> AI agents build, train, and analyze BCI/EEG pipelines: data, models, experiments, live sessions.

- name: io.github.nimbusbci/nimbus-mcp
- version: 0.14.11
- connection class: R0 (autonomous)
- trust: 94/100
- quality: 73/100 (needs work)
- owner: not claimed
- descriptor: https://api.protogrid.dev/v1/servers/io.github.nimbusbci%2Fnimbus-mcp
- tools: https://api.protogrid.dev/v1/servers/io.github.nimbusbci%2Fnimbus-mcp/tools

**An agent can connect right now, no human step.** Remote endpoint, no authentication, reachable on the last probe.

## Connection (mcpServers)

```json
{
  "mcpServers": {
    "nimbus-mcp": {
      "type": "http",
      "url": "https://nimbus-mcp.fly.dev/mcp"
    }
  }
}
```


## Trust

- hygiene: 100
- liveness: 99
- freshness: 100
- provenance: 80
- drivers: +repository +namespace-matches-repo +reachable +uptime-98% +updated-0d-ago
- Derived from observable signals (official registry feed and our own credential-free probes); no code audit performed.

## Quality 73/100 (needs work)

### protocol: 88 (weight 25)

- pass `protocol.modern`: supports 2026-07-28 (server/discover)
- pass `protocol.stateless`: answers without a session
- pass `protocol.transport`: streamable HTTP
- warn `protocol.list_ttl`: tools/list declares ttlMs 0, so clients re-fetch it on every use

### authorization: n/a (weight 15)

- n/a `auth.prm`, `auth.as_metadata`, `auth.cimd`: no remote uses OAuth
- n/a `auth.secret_in_url`: no templated URL

### tool hygiene: 86 (weight 30)

- pass `tools.descriptions`: every tool has a description
- warn `tools.description_length`: 3 tools have descriptions over 1,024 characters, which crowds the context
- pass `tools.schemas`: every tool has an object input schema
- pass `tools.annotations`: 60 of 60 tools declare readOnlyHint or destructiveHint
- pass `tools.directory_hints`: every tool declares readOnlyHint, destructiveHint, idempotentHint and openWorldHint
- warn `tools.token_cost`: about 13,129 tokens to load every tool
- pass `tools.api_dump`: tools are not a one-to-one API dump

### stability: 25 (weight 15)

- warn `stability.changes`: 135 tool changes in 30 days
- fail `stability.rug_pull`: 6 tools kept their name but changed most of their description in 30 days; review before trusting them

### dependencies: n/a (weight 15)

- n/a `deps.known_vulns`, `deps.mcp_sdk_version`, `deps.resolvable`: dependencies not resolved yet

- tools: 60, about 13,129 tokens to load them all
- tool set hash: 1312a91aa6f4cc7435b9e566a370419a
- tools last changed: 2026-10-20T00:23:47.439Z
- badge: [![protogrid quality](https://protogrid.dev/badge/io.github.nimbusbci/nimbus-mcp.svg)](https://protogrid.dev/servers/io.github.nimbusbci/nimbus-mcp)
- Checks run on what our credential-free, read-only probes observe; tools are never called and no code audit is performed.

### Tool changes

- 2026-10-20 `experiment.get` changed (description)
  - before: Experiment snapshot: status (running/completed/failed), per-run rows ({name, executionId, status, error?, metrics?, evaluationKind?}) and, once finished, aggregates over completed runs only: per-metric {mean, std, best, n}, plus ``pooledExecutionIds`` / ``pooledRunNames`` / ``kindBreakdown`` (per-subject vs LOSO etc. — each row has kind, n, mean, std, executionIds, runNames; #470). Pooled headline means are unchanged; kindBreakdown labels what was pooled. std is population; None below 2 values. kindBreakdown is always present when aggregates exist (including single-kind experiments). PORTABILITY: only experiments run through THIS entry point (local stdio vs hosted gateway), on this machine, under the same account/token can be read — records are kept in-process and in the local ~/.nimbus/experiments store, not on the Nimbus server (#518).
  - after: Experiment snapshot: status (running/completed/failed), per-run rows ({name, executionId, status, error?, metrics?, evaluationKind?}) and, once finished, aggregates over completed runs only: per-metric {mean, std, best, n}, plus ``pooledExecutionIds`` / ``pooledRunNames`` / ``kindBreakdown`` (per-subject vs LOSO etc. — each row has kind, n, mean, std, executionIds, runNames; #470). Pooled headline means are unchanged; kindBreakdown labels what was pooled. std is population; None below 2 values. kindBreakdown is always present when aggregates exist (including single-kind experiments). PORTABILITY: experiment records are stored on the Nimbus server under your account, so any entry point (local stdio, hosted gateway, another machine) signed in with the same account/token can read them; the serving process and the local ~/.nimbus/experiments store remain as fast-path caches. Experiments run before server-side storage landed exist only in the origin machine's local store and read as unknown elsewhere (#518).
- 2026-10-20 `experiment.run` changed (description)
  - before: Run 1-25 pipelines as ONE paced experiment (NON-BLOCKING). Returns an experimentId immediately; a background thread submits at most 2 runs at a time (min(max_concurrent, 2)), retries queue-full up to 3 times per run, and polls each execution to completion. Poll experiment.get() for per-run status and, once finished, aggregated metrics. PORTABILITY: experiment records live in the serving process and in this machine's local store (~/.nimbus/experiments) — they are NOT on the Nimbus server yet. Poll with the SAME entry point that ran the experiment (local stdio vs hosted gateway, same machine, same account/token): an experimentId from another entry point or machine reads as "Unknown experiment" there (#518).
  - after: Run 1-25 pipelines as ONE paced experiment (NON-BLOCKING). Returns an experimentId immediately; a background thread submits at most 2 runs at a time (min(max_concurrent, 2)), retries queue-full up to 3 times per run, and polls each execution to completion. Poll experiment.get() for per-run status and, once finished, aggregated metrics. PORTABILITY: the experiment record is written through to the Nimbus server under your account (at start and at completion), so experiment.get works from ANY entry point — local stdio, hosted gateway, another machine — signed in with the same account/token. If the server is unreachable when the experiment starts it still runs and polls locally, but other entry points cannot read it until a write-through succeeds (#518).
- 2026-10-20 `leaderboard.submit` changed (description, inputSchema)
  - before: Submit a BYO python_model to a leaderboard track for server-side scoring. Nimbus scores it server-side under the track's fixed protocol — you submit code, never results. Poll ``leaderboard.status`` with the returned ``submissionId``; scoring takes minutes (classical) to tens of minutes (deep). Requirements must be plain PyPI names/specs (max 20, no URLs). Identical code resubmission is idempotent per track (``deduplicated: true``).
  - after: Submit a BYO python_model to a leaderboard track for server-side scoring. Nimbus scores it server-side under the track's fixed protocol — you submit code, never results. Poll ``leaderboard.status`` with the returned ``submissionId``; scoring takes minutes (classical) to tens of minutes (deep). Requirements must be plain PyPI names/specs (max 20, no URLs). Identical code resubmission is idempotent per track (``deduplicated: true``). Verification contract (tier model X10): ``verification="verified"`` (default) is the server-side-scored submission above — on the hosted backend it is Pro-only; free accounts get a structured tier response. ``verification="unverified_local"`` names the designed Account-Free lane for locally-computed results; the backend has no unverified submission kind yet (see the response it returns — nothing is stored, and no row is faked). Anonymous callers get a structured login prompt: a submission needs an identity. Local (desktop/dev) backends keep their existing server-side quota behaviour.
- 2026-10-20 `account.whoami` changed (description)
  - before: Who you are authenticated as: account email, plan (isPro / pioneer), this month's free-run quota, and — with a hosted token — the token name and days until it expires. Call this first when setup guidance appears or to check which credential a session uses.
  - after: Who you are authenticated as: account email, plan (isPro / pioneer), this month's free-run quota, and — with a hosted token — the token name and days until it expires. Call this first when setup guidance appears or to check which credential a session uses. In anonymous Local Free mode there is no account: the response says so (mode/tier) instead of demanding a login.
- 2026-10-19 `foundation.compare` changed (description, outputSchema)
  - before: Run every available pretrained backbone on YOUR data and see which wins — a ranked bar chart with CI whiskers, params, and wall time per backbone. Backbones that can't process your data (window size, channel count) are noted, not silently skipped. Each backbone embeds your trials frozen (the exact foundation_features preprocessing), a LDA is cross-validated on the embeddings, and meta.results carries the per-backbone accuracy, CI, embed dim, params, and time — pick the foundation_features backbone by measurement, not vibes.
  - after: Run every available pretrained backbone on YOUR data and see which wins — a ranked bar chart with CI whiskers, params, and wall time per backbone. Backbones that can't process your data (window size, channel count) are noted, not silently skipped. Each backbone embeds your trials frozen (the exact foundation_features preprocessing), a LDA is cross-validated on the embeddings. Returns a PNG image PLUS a JSON text block (and structuredContent.meta.results) with per-backbone accuracy, CI, embed dim, params, and time — pick the foundation_features backbone by measurement, not vibes.
- 2026-10-19 `stats.grouped` changed (description, inputSchema)
  - before: Cluster-aware group statistics across executions. REQUIREMENT (read first): each execution must have been run with an evaluation plan that STORED per-group (per-subject) accuracies for at least 3 groups — e.g. leave-one-subject-out (LOSO) or grouped CV over >= 3 subjects. A plain single-subject CV run (trials split within one subject, one group) stores no group stats and CANNOT be analysed here. If none of the given executions qualifies, the backend fails with ``nimbus.stats.insufficient_groups`` — that error means exactly this: re-run with a multi-subject / group-wise evaluation plan (>= 3 groups), or pass runs from such a sweep. What it does: pools the per-subject (per-group) accuracies each run's evaluation plan stored, then reports the between-group summary — mean accuracy with a t-interval and a one-sample t-test vs chance. Use this instead of eyeballing pooled accuracy: trials within a subject are not independent, so this is the MOABB-style number that survives review.
  - after: Cluster-aware group statistics across executions. REQUIREMENT (read first): each execution must have been run with an evaluation plan that STORED per-SUBJECT accuracies for at least 3 subjects — e.g. leave-one-subject-out (LOSO) or grouped CV over >= 3 subjects. Session groups within one subject (multi-session single-subject runs) are refused with ``nimbus.stats.non_subject_groups`` — those are not independent subjects. A plain single-subject CV run with no subject/session grouping stores no group stats and fails with ``nimbus.stats.insufficient_groups``. What it does: pools the per-subject accuracies each run's evaluation plan stored, then reports the between-group summary — mean accuracy with a t-interval and a one-sample t-test vs chance. Responses label ``groupKind``, real ``groupKeys`` (subject ids like S01 when stamped or recoverable), ``perGroup`` (key + accuracy pairs), ``overall.pooledExecutionIds``, and ``kindBreakdown`` / ``overall.kindBreakdown`` (per_subject vs loso etc. with n + mean — #470; between-group pooled mean is unchanged).
- 2026-10-19 `project.save` changed (description, inputSchema) **meaning changed**
  - before: Save a pipeline graph into a project (visible on the studio canvas). Handles revision conflicts automatically (one retry).
  - after: Save a pipeline graph into a project (visible on the studio canvas). Always pass expected_revision from the last project.load (or a prior save/create response). When it does not match the current server revision, the save fails with a conflict — it does NOT silently overwrite concurrent edits. Omitting it is rejected so a stale single-writer convenience path cannot wipe another client's work.
- 2026-10-19 `data.delete_upload` added
  - Delete an uploaded file from your account (disk + registry). Requires confirm=True. Pass the path returned by data.upload (file.path). Only your own uploads can be deleted; missing paths return not-found.
- 2026-10-19 `data.upload` changed (description)
  - before: Upload an EEG file (.edf/.bdf/.mat/.csv/.txt/.tsv/.h5/.hdf5, <=500MB) to the backend and get the registered path for a custom_data node. sampling_rate (Hz, e.g. 250.0) is REQUIRED for plain CSV/TSV/TXT files without embedded metadata — the backend silently assumes 250 Hz otherwise, which mis-times epochs, filters and spectral features. format overrides extension-based detection (auto, mat, csv, tsv, txt, edf, bdf, h5, hdf5).
  - after: Upload an EEG file (.edf/.bdf/.mat/.csv/.txt/.tsv/.h5/.hdf5, <=500MB) to the backend and get the registered path for a custom_data node. sampling_rate (Hz, e.g. 250.0) is REQUIRED for plain CSV/TSV/TXT files without embedded metadata — uploads without it are rejected (no silent 250 Hz assumption). format overrides extension-based detection (auto, mat, csv, tsv, txt, edf, bdf, h5, hdf5).
- 2026-10-19 `plots.leaderboard` changed (outputSchema)

Full history: https://api.protogrid.dev/v1/servers/io.github.nimbusbci%2Fnimbus-mcp/changes

## Dependencies

- pypi nimbus-mcp@0.14.11: pending

Dependency graphs from deps.dev; advisories from OSV.dev, including the GitHub Advisory Database and the PyPI Advisory Database; all CC BY 4.0.

## Remotes

- https://nimbus-mcp.fly.dev/mcp (streamable-http, auth none, reachable true, uptime30d 0.983871)

## Packages

- pypi nimbus-mcp@0.14.11 secrets: NIMBUS_TOKEN, NIMBUS_MCP_KEY

## Tools (60)

- `account.whoami`: Who you are authenticated as: account email, plan (isPro / pioneer),
this month's free-run quota, and — with a hosted token — the token name
and days until it expires. Call this first when setup guidance appears
or to check which credential a session uses. In anonymous Local Free
mode there is no account: the response says so (mode/tier) instead of
demanding a login.
- `bids.export_dataset`: Export a Nimbus dataset as a BIDS-layout zip saved to NIMBUS_EXPORT_DIR/bids/.

Continuous sources (EDF/BDF uploads, stream recordings) export as BIDS-raw
(EDF byte-passthrough; recordings converted HDF5→EDF with events.tsv).
Epoched sources (MOABB packs, trial-table uploads) export as BIDS-derivatives
(data passthrough + descriptive trial_index/label/session/run events — no
fabricated onsets). The zip embeds nimbus_export_manifest.json with sha256s.
- `bids.export_execution`: Export an execution's results as a BIDS-derivative zip (metrics.json,
per-subject participants.tsv, protocol.json, full pipeline_snapshot.json,
predictions.tsv when stored, provenance/execution.json) saved to
NIMBUS_EXPORT_DIR/bids/.
- `calibration.pause`: Pause a running calibration between trials (cues hold; resume anytime).
- `calibration.resume`: Resume a paused calibration session.
- `calibration.start`: Start a guided subject calibration session (NON-BLOCKING; confirm-gated —
the device goes on a human's head). The Nimbus Studio app shows the cues on
its calibration dashboard automatically; poll calibration.status.
Requires a Pro plan (hosted token or Pro session): calibration nodes and
custom-data training are gated by the freemium node policy; local
X-MCP-Key principals pass only when the desktop app flags the signed-in
Pro session (MCP_LOCAL_IS_PRO).
- `calibration.status`: Live snapshot of a calibration session (phase, current trial, progress,
paused). Once complete, carries the recorded upload — call
calibration.train to turn it into the subject's own classifier.
- `calibration.train`: Train the subject's own classifier from a COMPLETED calibration session
(NON-BLOCKING). Fetches the recorded upload, wires it into a train
pipeline as a custom_data source, and starts the run. Requires a Pro
plan (custom_data training is freemium-gated). The calibrate→train
handoff resolves on every backend, desktop-local included; only
recordings made before the desktop execution-linkage build resolve
no upload — rerun calibration.start for those.
- `catalog.datasets`: Curated public EEG datasets (MOABB packs) available to pipelines.
- `catalog.leaderboard`: Public benchmark leaderboard: pipeline rankings per dataset.

Two tracks: the top-level ``within_session`` block (curated suite
pipelines) and ``crossSubject`` (leave-one-subject-out — curated
pipelines plus BYO ``python_model`` submissions; BYO rows carry
``kind: "byo"`` and an ``owner`` name). Within each dataset, ``rows``
sort desc by ``meanAccuracyPct`` (95% CI in ``ciLoPct``/``ciHiPct``).
``packFingerprint`` identifies the exact dataset pack behind the
scores; ``updated`` marks each dataset's most recent run. The
``pretrainedFewTrial`` efficiency track means: accuracy at a
protocol-fixed 100-trials-per-subject budget — the frozen-backbone /
few-parameter rows that matter when calibration data is scarce
(``paramsTrained`` shown per row so cost is readable next to
accuracy). ``submittableDatasets`` is the set open for
``leaderboard.submit``; a dataset can appear under ``crossSubject``
and still be closed for new submissions.
- `catalog.nodes`: List Nimbus pipeline node types (data, preprocessing, features, models...).

Use catalog.node_schema(node_type) for one node's full config schema and ports.
- `catalog.node_schema`: Full config JSON schema + input/output ports for one node type.
- `catalog.template`: Full template incl. the 'train' execGraph needed by execution.run/pipeline.validate.
- `catalog.templates`: List built-in starter pipelines (MI/P300/SSVEP...).
catalog.template(id) returns the graph.
- `data.delete_upload`: Delete an uploaded file from your account (disk + registry).

Requires confirm=True. Pass the path returned by data.upload
(file.path). Only your own uploads can be deleted; missing paths
return not-found.
- `data.inspect_dataset`: Exploratory summary of a public EEG dataset (MOABB pack): channels,
sampling rate, trial/class balance, per-channel µV stats, band powers
and a PSD overview.

Look at the data BEFORE building pipelines: class balance drives
stratification choices (imbalanced classes skew accuracy), and
flatlined channels mean a montage/reference problem worth fixing
first. subject is REQUIRED (the backend 400s without it) — get the
subject list via catalog.datasets, e.g. "S01"; a comma-list like
"S01,S03" loads a cohort. mode: training | evaluation | all.
Units note: values are ASSUMED volts by the loader — a µV-native file
reads 1e6x too large; set unitsScale in a pipeline's custom_data
config when needed.
- `data.inspect_file`: Exploratory summary of an EEG file (.edf/.bdf/.mat/.csv/.tsv/.txt/.h5):
channels, sampling rate, trial/class balance, per-channel µV stats,
band powers and a PSD overview.

The path shape picks the source: ABSOLUTE path → read the file from
disk (only on a LOCAL backend: desktop app / MCP local mode — no
upload needed); RELATIVE path (the one data.upload returns) → describe
the uploaded file, which works on ANY backend (hosted or local).

Look at the data BEFORE building pipelines: class balance drives
stratification choices, and flatlined channels mean a
montage/reference problem worth fixing first. Units note: values are
ASSUMED volts by the loader — a µV-native CSV reads 1e6x too large;
set unitsScale in a pipeline's custom_data config when needed. On a
hosted backend absolute paths are refused and this returns guidance
(upload the file first or switch to a local backend).
- `data.upload`: Upload an EEG file (.edf/.bdf/.mat/.csv/.txt/.tsv/.h5/.hdf5, <=500MB) to the
backend and get the registered path for a custom_data node.

sampling_rate (Hz, e.g. 250.0) is REQUIRED for plain CSV/TSV/TXT files
without embedded metadata — uploads without it are rejected (no silent
250 Hz assumption). format overrides extension-based detection
(auto, mat, csv, tsv, txt, edf, bdf, h5, hdf5).
- `device.list`: EEG devices supported by this backend (OpenBCI, Muse, BrainBit, LSL, PiEEG...).
- `device.test`: Test a device connection WITHOUT starting a stream (safe, no confirm needed).
- `execution.artifacts`: Trained artifacts (models/filters, e.g. *.pkl) saved by an execution.
- `execution.cancel`: Cancel a running execution.
- `execution.download_artifact`: Download one artifact file to NIMBUS_EXPORT_DIR/executions/<id>/ and return its path.
- `execution.get`: Execution status summary (status: running/completed/failed/cancelled).
- `execution.list`: Recent executions. Optional status filter (running/completed/failed/cancelled).
- `execution.results`: Metrics for a completed run. Trimmed by default (accuracy, kappa, ITR,
confusion matrix, per-class); full=True returns the complete result object.
- `execution.run`: Start a pipeline run (NON-BLOCKING). Returns executionId — poll with
execution.get() until status is completed/failed, then execution.results().
If an identical graph is already running for you, the run still starts but
the response carries a ``warning`` + ``duplicateOf`` ids (cancel one with
execution.cancel to avoid paying twice).
- `experiment.get`: Experiment snapshot: status (running/completed/failed), per-run rows
({name, executionId, status, error?, metrics?, evaluationKind?}) and,
once finished, aggregates over completed runs only: per-metric
{mean, std, best, n}, plus ``pooledExecutionIds`` / ``pooledRunNames``
/ ``kindBreakdown`` (per-subject vs LOSO etc. — each row has kind, n,
mean, std, executionIds, runNames; #470). Pooled headline means are
unchanged; kindBreakdown labels what was pooled. std is population;
None below 2 values. kindBreakdown is always present when aggregates
exist (including single-kind experiments).

PORTABILITY: experiment records are stored on the Nimbus server
under your account, so any entry point (local stdio, hosted
gateway, another machine) signed in with the same account/token
can read them; the serving process and the local
~/.nimbus/experiments store remain as fast-path caches.
Experiments run before server-side storage landed exist only in
the origin machine's local store and read as unknown elsewhere
(#518).
- `experiment.run`: Run 1-25 pipelines as ONE paced experiment (NON-BLOCKING). Returns an
experimentId immediately; a background thread submits at most 2 runs at
a time (min(max_concurrent, 2)), retries queue-full up to 3 times per
run, and polls each execution to completion. Poll experiment.get() for
per-run status and, once finished, aggregated metrics.

PORTABILITY: the experiment record is written through to the
Nimbus server under your account (at start and at completion), so
experiment.get works from ANY entry point — local stdio, hosted
gateway, another machine — signed in with the same account/token.
If the server is unreachable when the experiment starts it still
runs and polls locally, but other entry points cannot read it
until a write-through succeeds (#518).
- `foundation.compare`: Run every available pretrained backbone on YOUR data and see which wins —
a ranked bar chart with CI whiskers, params, and wall time per backbone.
Backbones that can't process your data (window size, channel count) are
noted, not silently skipped. Each backbone embeds your
trials frozen (the exact foundation_features preprocessing), a LDA is
cross-validated on the embeddings. Returns a PNG image PLUS a JSON text
block (and structuredContent.meta.results) with per-backbone accuracy,
CI, embed dim, params, and time — pick the foundation_features backbone
by measurement, not vibes.
- `leaderboard.mine`: List your leaderboard submissions (newest first), all datasets.
- `leaderboard.status`: Fetch one of your leaderboard submissions (owner-only).

Returns status (queued | scoring | done | failed), metrics when done
(meanAccuracyPct + CI, same shape as leaderboard rows), or the error
that failed it.
- `leaderboard.submit`: Submit a BYO python_model to a leaderboard track for server-side scoring.

Nimbus scores it server-side under the track's fixed protocol — you
submit code, never results. Poll ``leaderboard.status`` with the
returned ``submissionId``; scoring takes minutes (classical) to tens
of minutes (deep). Requirements must be plain PyPI names/specs (max
20, no URLs). Identical code resubmission is idempotent per track
(``deduplicated: true``).

Verification contract (tier model X10): ``verification="verified"``
(default) is the server-side-scored submission above — on the hosted
backend it is Pro-only; free accounts get a structured tier response.
``verification="unverified_local"`` names the designed Account-Free
lane for locally-computed results; the backend has no unverified
submission kind yet (see the response it returns — nothing is
stored, and no row is faked). Anonymous callers get a structured
login prompt: a submission needs an identity. Local (desktop/dev)
backends keep their existing server-side quota behaviour.
- `pipeline.export`: Export the pipeline as a standalone runnable Python bundle (zip saved locally).
- `pipeline.validate`: Validate a pipeline graph before running.
ExecGraphSnapshot: {nodes: [{id, type, config}], connections: [{from, to}]}.
Build it from catalog.template(id).train or from scratch using catalog.nodes().
- `pipeline.validate_node`: Validate one node's config (schema + semantic checks).

Catches the same value errors as pipeline.validate for a single node
(inverted bandpass, CSP components vs channel count when dataset is set).
- `plots.confusion`: Confusion-matrix figure for one execution: heatmap with trial
counts + per-class accuracy bars, rendered as a PNG the assistant can
see. Use it right after execution.results to eyeball WHICH classes are
confused, not just the accuracy number.
- `plots.dataset`: Figure over an EEG data source — the visual companion to
data.inspect_dataset. kind=psd (Welch, channel mean bold),
kind=bandpower (channel × band dB heatmap), kind=topomap (band-power
scalp map), kind=spectrogram (time-frequency for one channel). Sources
are the same ones inspect uses: MOABB packs, uploads, local files.
- `plots.erp`: ERP butterfly figure from epoched data: one subplot per class, all
channel means (thin) + across-channel mean (bold), time-locked to the
trial event. Shows whether class-conditioned evoked structure exists
BEFORE training anything.
- `plots.leaderboard`: Leaderboard bar chart for one dataset: accuracy with 95% CI
whiskers, best first; BYO python_model submissions highlighted in a
distinct color. The visual companion to catalog.leaderboard. On the
pretrained_few_trial track the figure also includes the classical
LOSO baseline as a muted side panel for comparison.
- `project.create`: Create a project (container for one pipeline document). Returns projectId.
- `project.delete`: Delete a project (and its saved pipeline document) from the studio workspace.

Requires confirm=True: this removes the project from the user's project
list. Executions already run from it are not affected. Find ids with
project.list.
- `project.list`: List projects owned by the current principal (agent work included).
- `project.load`: Load a project's saved pipeline (train graph + meta) for editing/re-running.
- `project.save`: Save a pipeline graph into a project (visible on the studio canvas).

Always pass expected_revision from the last project.load (or a prior
save/create response). When it does not match the current server
revision, the save fails with a conflict — it does NOT silently
overwrite concurrent edits. Omitting it is rejected so a stale
single-writer convenience path cannot wipe another client's work.
- `protocol.export_stats`: Paired pre/post analysis for one assignment's recorded sessions:
per-block aggregates (descriptive + CSV), paired t / Wilcoxon / dz
(per-statistic minimums: paired t and Cohen's dz need ≥ 3 completed
sessions, Wilcoxon needs ≥ 5), specificity notes, the
adaptive-threshold trajectory, and a pre/post figure (base64 PNG —
render or save it to look at the effect). An assignment with no
recordings yet returns the empty payload with a note, never an error.
- `protocol.list`: Every saved protocol id at its latest version.
- `protocol.pause`: Pause a running protocol session (blocks freeze; the session and
device stay alive — resume with protocol.resume). Simulated and
playback sessions only while paused: no data is recorded.
- `protocol.resume`: Resume a paused protocol session (restarts the server-driven device
loop). Live-hardware resume requires a Pro plan; simulated sessions are
free. Pass the session's channel count (start_assignment reports it).
- `protocol.save`: Validate and persist the NEXT VERSION of a protocol document
(confirm-gated: each call appends a version row; accidental calls
clutter the version history). Edits create a new version — the
assignment pins the version it runs.
- `protocol.session_status`: Live snapshot of a protocol deploy session: the scheduler block /
session progress + threshold state (under ``protocol``) beside the
telemetry window (recent indicators, quality, stats).
- `protocol.start_assignment`: Enroll a subject and prepare their neurofeedback deploy session
(confirm-gated — a session runs on a human). Creates the assignment
(yoked sham arms REQUIRE yoked_donor_upload_id: a recorded simulated
session's upload — mint one by running an active-arm session and
data.upload-ing its recording), then persists the protocol graph as a
deployable execution. Requires a Pro plan for LIVE hardware sessions;
simulated-source sessions (the default template) are free.

Graph source: execution_id reuses THAT run's stored graph (it must
contain a protocol_session node; no device is pre-connected — the
deploy session's data source stays yours). Without execution_id the
built-in template graph is cloned — simulated source → bandpass →
bandpower_indicator → protocol_session stamped with the protocol refs —
and a simulated device is connected to the returned session id.

The session starts when a client opens {statusUrl} (the deploy WS),
sends init_deployment with modelExecutionId={executionId}, then start;
the Studio app shows the feedback panel automatically. Poll
protocol.session_status; stop with protocol.stop (the recording's
block annotations + assignment progression are written on stop).
- `protocol.stop`: Stop a protocol session (always safe to call): stops streaming,
saves the block-annotated recording, advances the assignment when the
scheduler reached its natural end, and disconnects the device
(best-effort — a failed disconnect is reported, never raised).
- `protocol.validate`: Validate a neurofeedback protocol document WITHOUT saving it.
Returns {valid: true, spec} on success, or {valid: false, errors: [...]}
with the per-violation list (loc/msg) when the document is invalid —
the verdict is data, never an exception.
- `python.validate`: Statically check a python_model script (BYO model class) without running it.

Compiles the source and verifies the contract — a class with
fit(X, y, info) and predict(X); optional predict_proba(X) unlocks
confidence panels. Never executes the code, so it is safe against the
hosted backend. Use it before execution.run to catch syntax errors,
missing methods, or the wrong class name cheaply.
- `stats.grouped`: Cluster-aware group statistics across executions.

REQUIREMENT (read first): each execution must have been run with an
evaluation plan that STORED per-SUBJECT accuracies for at least 3
subjects — e.g. leave-one-subject-out (LOSO) or grouped CV over
>= 3 subjects. Session groups within one subject (multi-session
single-subject runs) are refused with
``nimbus.stats.non_subject_groups`` — those are not independent
subjects. A plain single-subject CV run with no subject/session
grouping stores no group stats and fails with
``nimbus.stats.insufficient_groups``.

What it does: pools the per-subject accuracies each run's evaluation
plan stored, then reports the between-group summary — mean accuracy
with a t-interval and a one-sample t-test vs chance. Responses label
``groupKind``, real ``groupKeys`` (subject ids like S01 when stamped
or recoverable), ``perGroup`` (key + accuracy pairs),
``overall.pooledExecutionIds``, and ``kindBreakdown`` / 
``overall.kindBreakdown`` (per_subject vs loso etc. with n + mean —
#470; between-group pooled mean is unchanged).
- `stream.start`: Connect an EEG device and START a live streaming session on the user's head.
Requires confirm=True; call device.test first. Track with stream.status().
Idle watchdog: if no stream.status()/stream.telemetry() poll happens for
idle_timeout_sec (default 900), the session is stopped and the device
disconnected automatically — an abandoned stream never keeps running on
the user's head. Any poll resets the timer; idle_timeout_sec=0 disables
the watchdog.
- `stream.status`: Live snapshot of a streaming session (running, deviceConnected).
Polling this also feeds the idle watchdog: each call resets the
session's idle timer (see stream.start's idle_timeout_sec).
- `stream.stop`: Stop a streaming session and disconnect the device (always safe to call).
Also removes the session from the idle watchdog so it cannot fire after
an explicit stop.
- `stream.telemetry`: Live snapshot of a streaming session: latest prediction + recent window,
signal quality (meanChannelQuality, snrDb, artifactProbability), indicators,
running stats. Poll this while a session runs. Works with OR without a
deployed model: modelless hardware streams (no classifier) report live
signal quality + focus/relaxation band-power indicators with
latestPrediction null; deployed sessions additionally carry predictions
and confidence stats. Expect low
confidence during filter/ASR warm-up (first seconds); quality < 0.5 or high
artifactProbability means the signal is poor. 404 => session not active in
this backend. Each poll also feeds the idle watchdog (see stream.start's
idle_timeout_sec), keeping an actively watched session alive.
