nimbus-mcp
Official# nimbus-mcp
<!-- mcp-name: io.github.nimbusbci/nimbus-mcp -->
[](https://cursor.com/en/install-mcp?name=nimbus&config=eyJ1cmwiOiJodHRwczovL25pbWJ1cy1tY3AuZmx5LmRldi9tY3AifQ==)
[](https://insiders.vscode.dev/redirect/mcp/install?name=nimbus-bci&config=%7B%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fnimbus-mcp.fly.dev%2Fmcp%22%7D)
[](https://m8ven.ai/mcp/nimbusbci-nimbus-mcp-1xjm62?s=readme)
MCP server that lets AI agents (Claude Code, Cursor, Claude Desktop) build, validate,
run, and analyze Nimbus BCI pipelines — upload their own EEG data, persist pipelines
into studio projects, run multi-configuration experiment campaigns on public
MOABB/BNCI datasets, render plots, and export BIDS — through your **local**
Nimbus backend **or the hosted deployment, with one `nimbus-mcp login`**.
**Docs:** [docs.nimbusbci.com](https://docs.nimbusbci.com) · **Overview:** [nimbusbci.com/mcp](https://nimbusbci.com/mcp) · Jump to [Quickstart](#quickstart-hosted-recommended)
## Quickstart (hosted, recommended)
Nothing to install: point your MCP client at the hosted server
`https://nimbus-mcp.fly.dev/mcp`. On first use your client opens a browser
sign-in to Nimbus Studio (OAuth) — no token to paste.
**Cursor** — click the **Add to Cursor** button above, or put this in
`.cursor/mcp.json` (or `~/.cursor/mcp.json`):
```json
{
"mcpServers": {
"nimbus": { "url": "https://nimbus-mcp.fly.dev/mcp" }
}
}
```
**Claude Code:**
```bash
claude mcp add --transport http nimbus https://nimbus-mcp.fly.dev/mcp
```
**Claude Desktop / claude.ai:** Settings → Connectors → *Add custom connector*
→ URL `https://nimbus-mcp.fly.dev/mcp`. (Custom connectors may require a
paid Claude plan.)
**Other clients (streamable HTTP):**
```json
{
"mcpServers": {
"nimbus": { "type": "http", "url": "https://nimbus-mcp.fly.dev/mcp" }
}
}
```
**Headless / CI (no browser):** OAuth stays the primary path, but the
gateway also accepts a Nimbus API key (create one on the Studio Account page)
as a Bearer header:
```json
{
"mcpServers": {
"nimbus": {
"type": "http",
"url": "https://nimbus-mcp.fly.dev/mcp",
"headers": { "Authorization": "Bearer ${NIMBUS_TOKEN}" }
}
}
}
```
```bash
claude mcp add --transport http nimbus https://nimbus-mcp.fly.dev/mcp \
--header "Authorization: Bearer $NIMBUS_TOKEN"
```
### Try it in 60 seconds
After sign-in, paste this prompt into your agent:
> Benchmark a CSP + LDA motor-imagery pipeline on BCI IV 2a (`BNCI2014_001`),
> show me the confusion matrix, and compare the score to the leaderboard.
You should see the agent call `catalog.datasets` / `catalog.templates`,
start a run with `execution.run`, poll `execution.get`, then show
`execution.results` (accuracy / kappa), a `plots.*` figure and the
`catalog.leaderboard` comparison — all on public MOABB data, no EEG device
needed. The longer guided loop is in [examples/demo.md](examples/demo.md).
The server also ships 5 ready-made MCP prompts (your client lists them as
slash commands / prompt templates): `mi_csp_lda_bci_iv_2a`,
`compare_mi_pipelines`, `foundation_backbones`, `inspect_my_eeg` and
`leaderboard_standing`.
## Why nimbus-mcp (October 2026 field)
The BCI-MCP niche grew four other servers in 2026. They are honest,
focused tools — and each covers a slice of what nimbus-mcp does end-to-end:
| Capability | nimbus-mcp | bci-mcp | eeg-mcp | mne-mcp | ephys-mcp |
|---|---|---|---|---|---|
| Build + validate + run BCI pipelines | ✅ 63 tools | — | — | — | — |
| Train on real data (MOABB packs, uploads) | ✅ | — | — | file I/O only | NWB/DANDI |
| Bring-your-own model code (`python_model`) | ✅ local + cloud sandbox | — | — | — | — |
| Benchmark leaderboard + server-side LOSO scoring | ✅ | — | — | — | — |
| Agent-visible figures (PNG content blocks) | ✅ `plots.*` | — | — | ✅ | ✅ |
| Modelless live band powers (focus/relaxation) | ✅ `stream.telemetry` | ✅ | ✅ | — | — |
| Guided subject calibration | ✅ `calibration.*` | — | — | — | — |
| Neurofeedback protocol sessions | ✅ `protocol.*` | — | — | — | — |
| BIDS export (sha256 manifests) | ✅ `bids.*` | — | — | — | — |
| Cluster-aware group statistics | ✅ `stats.grouped` | — | — | — | partial |
| Transport | stdio + hosted HTTP gateway | stdio + HTTP | stdio | stdio | stdio |
| Validation evidence | [arXiv:2610.13894](https://arxiv.org/abs/2610.13894) — parity study, 3 datasets / 6 pipelines / 2 protocols | band-ratio heuristics | — | — | — |
Scope honesty (deliberate non-goals): **no stimulation output** (eeg-mcp's
triple-interlocked `send_stim_event` is genuinely novel; we don't drive
stimulators), **no spike/ephys data** (ephys-mcp covers intracortical
recording — different data plane), **no source localization / full MNE
parity** (mne-mcp is a deep offline MNE wrapper; `plots.*` covers the
figures agents need on Nimbus data).
## Install locally (stdio)
Prefer running the server on your own machine (or using your local
Studio desktop backend)? Sign in once, then use a secret-free config:
```bash
uvx nimbus-mcp login # or: pip install nimbus-mcp && nimbus-mcp login
```
```json
{
"mcpServers": {
"nimbus": { "command": "uvx", "args": ["nimbus-mcp"] }
}
}
```
Both `uvx nimbus-mcp` and `python -m nimbus_mcp` (inside the env where it is installed) start the
stdio MCP server. Agent-oriented install steps (e.g. for Cline): [llms-install.md](llms-install.md).
(Also installable from the repo: `pip install -e nimbus-studio/mcp`.)
**Zero-hardware demo**: [examples/demo.md](examples/demo.md) — a copy-paste
agent prompt that runs the whole loop (explore → inspect → figures → train →
confusion figure → leaderboard → stats) on public MOABB data, no device.
## Authentication
Three ways to give the server a credential — tried in this order at startup:
1. **`nimbus-mcp login` (recommended — hosted API, no token pasting).** A
device-code login: the command prints a URL and an 8-character code, opens
your browser, you approve in Nimbus Studio, and the minted token is stored
at `~/.nimbus/credentials.json` (0600) and picked up automatically on every
future start.
```bash
nimbus-mcp login # options: --api-url URL, --ttl-days 7..90, --name NAME
nimbus-mcp status # doctor: credential source, plan, quota, days-to-expiry, live probe
nimbus-mcp logout # remove the stored credential
```
After a login, MCP client configs need no secret at all:
```json
{
"mcpServers": {
"nimbus": {
"command": "uvx",
"args": ["nimbus-mcp"]
}
}
}
```
2. **Desktop app — zero config.** Just have the Nimbus Studio desktop app
running: its local key file is auto-discovered (macOS `~/Library/Application
Support/Nimbus Studio/mcp-key.json`, Linux `~/.config/Nimbus Studio/…`,
Windows `%APPDATA%\Nimbus Studio\…`) and the server talks to the local
backend at `http://127.0.0.1:8080`. Nothing to paste or configure.
3. **Environment variables (advanced / CI).** `NIMBUS_TOKEN` (a hosted API
token `nimb_…` minted in Nimbus Studio → Account → API tokens) or
`NIMBUS_TOKEN_FILE` (a 0600 JSON file `{"token": "…"}` — keeps the secret
out of process env and MCP configs), or the local pair
`NIMBUS_MCP_KEY` / `NIMBUS_MCP_KEY_FILE` (must match `MCP_LOCAL_KEY` on a
local backend — see below). Explicit env always beats files on disk.
A `NIMBUS_TOKEN` / `NIMBUS_TOKEN_FILE` token with `NIMBUS_API_URL` unset
talks to the hosted API (`https://nimbus-studio.fly.dev`); set
`NIMBUS_API_URL` to point it anywhere else.
**No credential anywhere?** That is the **Local Free tier** — it works. The
server starts anonymously against the local backend (desktop app or dev
backend on `http://127.0.0.1:8080`): templates, your own data, local
execution/plots/stats, BIDS export and local-only storage need **no Nimbus
Studio login at all**. Login is requested only at first *cloud* value — a
tool that needs an identity (leaderboard submission, verified/cross-machine
experiment history, hosted compute, cloud sync, collaboration) returns
`{ok: false, loginRequired: true, trigger, reason, howToAuthenticate}`
naming exactly why, instead of a blanket gate. Hosted Pro features
(verified leaderboard submissions, `foundation.compare`, big campaigns,
team features) return `{ok: false, tierGate: true, requiredTier: "pro"}`
on free accounts; exports (BIDS etc.) are never gated at any tier. If a
local call fails because no local backend/identity exists, the response is
the local-mode variant of `{ok: false, setupRequired: true, message,
options}` (start the desktop app / set a dev key — still no login). A
hosted token rejected mid-session (expired or revoked) returns the classic
setup shape, including how many days ago it expired.
### Checking who you are
```
account.whoami() → {userId, email, plan: {isPro, pioneerAccess},
freeRuns: {monthlyLimit, remaining},
token: {name, kind, scopes, fullAccess, expiresAt, daysLeft}
| {masked, kind, scopes, fullAccess, expiresAt, daysLeft} # gateway header token
| null,
source} # store | env | token_file | header | …
```
Call `account.whoami()` from the agent to see the account, plan, this month's free-run
quota, and (in token mode) the token's days-to-expiry; `nimbus-mcp status` is
the terminal equivalent with a live backend probe.
### What a hosted token means
- **The token IS you.** Requests run under your account: executions appear in your
studio history and **your plan's quotas and limits apply** — there is no separate
agent allowance. When the free monthly quota is exhausted, run errors carry the
upgrade link `https://studio.nimbusbci.com/pricing?reason=mcp-quota`.
- **CPU-only in v0.4.** Token-authenticated runs do not hydrate cloud GPUs.
- **Rotation.** Tokens live at most 90 days (30 by default). Plan changes are
snapshotted at mint time — after an upgrade, re-login (or revoke and re-create
the token) to pick up the new plan. An expired token surfaces as setup
guidance with the day count, not a dead end.
### Headless agents and CI (API keys)
For CI pipelines and unattended agents, mint a **CI key** in Nimbus Studio →
Account → API tokens: choose type **CI / headless**, a lifetime of 90, 180 or
365 days, and pick its scopes. **CI keys are a Pro feature** (Pro plan or
Pioneer access); Free accounts can still use personal tokens.
| Scope | Allows |
|---|---|
| `read` | GET/list/describe, plus compute-only POSTs (validate, plots, stats, export) |
| `run` | start/cancel executions and experiments (consumes your quota) |
| `write` | create/update/delete projects, pipelines, uploads, settings |
| `leaderboard:submit` | leaderboard submissions |
CI keys default to `read` + `run`; a full-access CI key is an explicit choice
(the **Full access** checkbox, or `"fullAccess": true` on
`POST /api/me/api-tokens`). Personal tokens keep the old default: no scope
selected = full access, and every token minted before scopes existed keeps
working unchanged. The backend enforces scopes on every request; a missing
scope answers `403 nimbus.auth.token_scope_insufficient`. `account.whoami` and
`nimbus-mcp status` show the key's kind, scopes and days left.
Store the key as a CI secret and pass it as `NIMBUS_TOKEN` (no `NIMBUS_API_URL`
needed — a token defaults to hosted Nimbus). GitHub Actions example:
```yaml
- name: Nimbus smoke run
env:
NIMBUS_TOKEN: ${{ secrets.NIMBUS_CI_KEY }}
run: |
pipx install nimbus-mcp
nimbus-mcp status # fails fast if the key is expired or revoked
```
Or call the hosted gateway `https://nimbus-mcp.fly.dev/mcp` with the header
`Authorization: Bearer $NIMBUS_TOKEN`. Rotate before expiry (the Account page
flags keys within 14 days of expiring) and revoke a leaked key immediately —
revocation takes effect on the next request.
Plan and limits for API keys:
- Plan is re-checked live: every request made with an API key looks up your
current Clerk Billing plan (cached for about a minute), so cancelling Pro
removes Pro features from existing keys too — not just from new ones.
- If the owner of a CI key is no longer Pro, the key is refused on every
request with `403 nimbus.auth.ci_key_requires_pro`. Resubscribing makes it
work again (no need to re-mint); otherwise revoke it and switch to a
personal token.
- You can have at most **20 active** (unrevoked, unexpired) API tokens;
minting one more answers `409 nimbus.me.api_token_limit_reached`. Tokens
created by MCP OAuth sign-in do not count toward this limit.
## Privacy & telemetry
Local Free mode needs no account and phones home nothing by default.
Anonymous, content-free usage telemetry (tool names, success/failure and
error codes, duration buckets — never your data, file paths, or code) is
**opt-in**: you are asked once in the first tool response of a fresh
install, and `nimbus-mcp telemetry on|off|status` (or
`NIMBUS_MCP_TELEMETRY=on|off`) controls it anytime. With telemetry off
there are zero telemetry network calls, and everything local keeps
working. Full statement: [PRIVACY.md](PRIVACY.md). A quiet PyPI version
check at startup (cached 24 h, silent when current or offline) adds a
one-time `versionNotice` to a tool response when your client is behind
the hosted gateway's surface.
## Requirements (local mode)
- A Nimbus backend running locally: the **desktop app**, or the dev server
(`cd nimbus-studio/backend-py && python -m nimbus_backend.server.app`) with `DEBUG=1`.
- The backend started with `MCP_LOCAL_KEY=<some-secret>` (never set this on Fly — it is
refused there).
- Desktop app users: open **Settings → MCP & Agents** — no manual key setup (the app
creates the key, injects it into its backend, and hands you copy-ready configs).
**No desktop app required.** Acquisition is fully in-backend: a headless local
backend + `nimbus-mcp` can drive real hardware (BrainFlow boards, BrainBit over
BLE, LSL, PiEEG) and synthetic boards, with zero Electron:
```bash
git clone <this repo> && cd NimbusStudio/nimbus-studio/backend-py
pip install -e ".[streaming]" # brainflow + pyneurosdk2 + pylsl extras
export DESKTOP_LOCAL_BACKEND=true MCP_LOCAL_KEY=<secret> MCP_LOCAL_USER_ID=user_…
python -m nimbus_backend.server.app # loopback backend with device radios
# another shell: nimbus-mcp against http://127.0.0.1:8080 with NIMBUS_MCP_KEY=<secret>
```
The same `stream.start → stream.telemetry` flow then works with OR without a
trained model (modelless streams report live signal quality + focus/relaxation
indicators).
## Configure the backend
Desktop/dev env (e.g. `backend-py/data/.env` or the dev shell):
```bash
MCP_LOCAL_KEY=choose-a-long-random-string
MCP_LOCAL_USER_ID=user_your_clerk_user_id
DEBUG=1 # dev server only; the desktop app qualifies automatically
```
`MCP_LOCAL_USER_ID` sets the principal the MCP key authenticates as. Set it to your
own Clerk user id (`user_…`) so everything the agent creates — projects, saved
pipelines, executions — appears in your studio UI as yours. Pick one owner and stick
with it: switching the id mid-life splits ownership of agent-created work across two
principals, and neither identity then sees the whole history.
Watchdog default: streaming sessions started through MCP are auto-stopped after
15 minutes with no one watching (every `stream.status` / `stream.telemetry` poll
resets the timer). Pass `idle_timeout_sec=0` to `stream.start` to disable it for a
session.
When enabling `MCP_LOCAL_KEY` on a machine connected to an untrusted network, also set
`HOST=127.0.0.1` on the backend. The `0.0.0.0` default (`settings.host`) applies to the
bare dev server (`python -m nimbus_backend.server.app`), so with it the key would
otherwise be accepted from the LAN; `backend-py/scripts/run_server.py` already defaults
to `127.0.0.1`, and the desktop app pins loopback itself.
## Run the server
```bash
cd nimbus-studio/mcp
python -m venv .venv && source .venv/bin/activate
pip install -e ".[test]"
NIMBUS_MCP_KEY=choose-a-long-random-string python -m nimbus_mcp
```
Env vars: `NIMBUS_API_URL` (always wins when set; otherwise the store's
`api_url` after a login, `https://nimbus-studio.fly.dev` for a
`NIMBUS_TOKEN` / `NIMBUS_TOKEN_FILE` token, else `http://127.0.0.1:8080`), `NIMBUS_TOKEN` / `NIMBUS_TOKEN_FILE` (hosted API
token — see **Authentication**), `NIMBUS_MCP_KEY` (must match `MCP_LOCAL_KEY`),
`NIMBUS_MCP_KEY_FILE` (path to a 0600 JSON file `{"key": "…"}` — the desktop
app's one-click MCP setup writes it; consulted only when `NIMBUS_MCP_KEY` is
unset), `NIMBUS_EXPORT_DIR` (default `~/nimbus-exports`). With none of the
token/key vars set, the login store and then the desktop key file are
auto-discovered; with nothing found, the server runs in setup mode (every tool
returns onboarding guidance).
## Self-hosting the gateway (streamable HTTP)
The public gateway `https://nimbus-mcp.fly.dev/mcp` uses OAuth sign-in for
interactive clients and also accepts `Authorization: Bearer <API key>` for
headless/CI use — see [Quickstart](#quickstart-hosted-recommended). This
section is for running your own.
`nimbus-mcp serve` runs the same 63 tools over streamable HTTP instead of
stdio — for remote MCP clients, registries (Smithery lists URL-based
servers), and browser-side clients:
```bash
nimbus-mcp serve --port 8080 # env: NIMBUS_MCP_HOST/PORT/PATH
```
The gateway binds **127.0.0.1 by default** — exposing it (`--host 0.0.0.0`)
is explicit opt-in; combine that with the per-request tokens below (the
gateway itself holds no credential to gate access with).
The gateway holds **no** credential itself: each request's Nimbus token
arrives per request, so one shared deployment serves many users as
themselves. With OAuth on (below, and on the public gateway) clients sign in
in the browser. On a self-hosted gateway **without** OAuth, send the token as
an `X-Nimbus-Token` or `Authorization: Bearer nimb_…` header; headerless calls
then get setup guidance:
```json
{
"mcpServers": {
"nimbus": {
"type": "http",
"url": "http://127.0.0.1:8080/mcp",
"headers": { "X-Nimbus-Token": "nimb_… (Account → API tokens)" }
}
}
}
```
(A gateway started with `NIMBUS_TOKEN` in the environment uses it as the
fallback for headerless calls — single-tenant self-hosting.)
### OAuth sign-in (opt-in, `NIMBUS_MCP_OAUTH=1`)
With `NIMBUS_MCP_OAUTH=1` the gateway is also an OAuth 2.1 authorization
server (MCP authorization spec), so clients like Claude and Cursor can connect
with just the URL — no pasted token:
- `/.well-known/oauth-protected-resource/mcp` and
`/.well-known/oauth-authorization-server` metadata;
- dynamic client registration (`/register`, stored by the backend);
- `/authorize` (PKCE S256) sends the browser to Studio
`/mcp/authorize`, where the user signs in and chooses Allow / Deny;
- `/token` exchanges the code for a normal `nimb_` token (30 days, no
refresh token; scopes `read`, `run`, `write`, `leaderboard:submit`, or full
access when all/none are requested). Re-authorizing the same client
revokes that client's previous token, and every OAuth token is listed (and
revocable) under Account → API tokens.
When the flag is on, requests with no token get the spec `401` with a
`WWW-Authenticate: Bearer resource_metadata=…` header (that is what starts the
client's sign-in), so headerless `tools/list` is no longer public. Existing
`X-Nimbus-Token` / `Authorization: Bearer nimb_…` headers keep working.
| Variable | Where | Meaning |
| --- | --- | --- |
| `NIMBUS_MCP_OAUTH` | gateway | `1` turns OAuth on (default off) |
| `MCP_OAUTH_GATEWAY_SECRET` | gateway **and** backend | shared secret (≥ 32 chars) for the gateway-only backend routes and for signing the consent request |
| `NIMBUS_MCP_PUBLIC_URL` | gateway | issuer / public base URL (default `https://nimbus-mcp.fly.dev`; must be HTTPS except on loopback) |
| `NIMBUS_STUDIO_URL` | gateway | where `/mcp/authorize` lives (default `https://studio.nimbusbci.com`) |
| `NIMBUS_API_URL` | gateway | backend base URL |
On Fly: `fly secrets set MCP_OAUTH_GATEWAY_SECRET=… NIMBUS_MCP_OAUTH=1` for the
gateway app, and the same `MCP_OAUTH_GATEWAY_SECRET` on the backend. With the
secret unset the backend answers the OAuth routes with `503
nimbus.mcp.oauth_not_configured`.
## Claude Code (local backend, advanced)
For a hosted setup use the one-liner in [Quickstart](#quickstart-hosted-recommended).
This form runs a dev checkout against a local backend with `MCP_LOCAL_KEY`:
```bash
# --env flags go BEFORE the -- separator (everything after it is the literal
# server command, so the after-form would feed --env to python/uvx):
claude mcp add nimbus --env NIMBUS_MCP_KEY=choose-a-long-random-string \
-- <path-to-mcp-venv>/bin/python -m nimbus_mcp
```
## Cursor / Claude Desktop (stdio, local backend, advanced)
Hosted or `uvx` setups need none of this — see
[Quickstart](#quickstart-hosted-recommended) and
[Install locally](#install-locally-stdio).
```json
{
"mcpServers": {
"nimbus": {
"command": "<path-to-mcp-venv>/bin/python",
"args": ["-m", "nimbus_mcp"],
"env": { "NIMBUS_MCP_KEY": "choose-a-long-random-string" }
}
}
}
```
## Tools (63)
Auth: `account.whoami` (account, plan, quota, token expiry)
Discovery: `catalog.nodes`, `catalog.node_schema`, `catalog.templates`, `catalog.template`, `catalog.datasets`, `catalog.leaderboard`
Data: `data.upload`, `data.delete_upload` (needs `confirm=true`)
Inspect: `data.inspect_dataset`, `data.inspect_file` (EDA: channels, class balance, band powers, PSD)
Build: `pipeline.validate`, `pipeline.validate_node`
Run: `execution.run` (non-blocking), `execution.get`, `execution.list`, `execution.results`, `execution.cancel`, `execution.delete` (needs `confirm=true`)
Run summaries (`execution.get` / `execution.list`) carry two dataset fields: `dataset` is the human-readable display label (e.g. `BCIIV2a`, i.e. BCI Competition IV 2a), and `datasetId` is the catalog id (e.g. `BNCI2014_001` — the same dataset) that other tools accept (`data.inspect_dataset`, `public_data.dataset` in a graph). `title` is the run name shown in Studio.
Campaigns: `experiment.run` (non-blocking, 1-25 paced runs), `experiment.get`, `experiment.delete` (needs `confirm=true`)
Artifacts: `execution.artifacts`, `execution.download_artifact`, `pipeline.export`
Live: `device.list`, `device.test`, `stream.start` (needs `confirm=true`),
`stream.status`, `stream.telemetry`, `stream.stop`
Calibration: `calibration.start` (needs `confirm=true`), `calibration.status`,
`calibration.pause`, `calibration.resume`, `calibration.train`
Neurofeedback protocols: `protocol.validate`/`protocol.save` (versioned specs),
`protocol.list` (saved protocols with their versions),
`protocol.start_assignment` (confirm-gated; simulated sources free, live hardware Pro),
`protocol.session_status`/`pause`/`resume`/`stop`, `protocol.export_stats`
(paired pre/post stats + figure + CSV). Since 0.14.12 neither `protocol.save`
nor `protocol.start_assignment` accepts a `name` argument (it was never
persisted — versions carry `spec.name`).
Projects: `project.create`, `project.list`, `project.save`, `project.load`, `project.rename`, `project.delete` (needs `confirm=true`)
Python: `python.validate` (static contract check for BYO model code — no execution)
Leaderboard: `leaderboard.submit` (BYO python_model → server-side LOSO scoring,
202 + `submissionId`; `track="pretrained_few_trial"` = efficiency track:
100-trial-capped LOSO, torch code must declare `PARAMS_TRAINED`),
`leaderboard.status` (poll one submission, owner-only),
`leaderboard.mine` (your submissions, newest first)
BIDS: `bids.export_dataset` (pack / upload / recording → BIDS-layout zip;
continuous → BIDS-raw, epoched → BIDS-derivatives), `bids.export_execution`
(results bundle: metrics, participants.tsv, protocol, pipeline snapshot)
Plots: `plots.confusion`, `plots.dataset` (psd / topomap / bandpower /
spectrogram), `plots.erp` (per-class butterfly), `plots.leaderboard` (CI
whisker bars, BYO highlighted; `track=pretrained_few_trial` overlays the
classical LOSO baseline side-by-side) — PNG figures the assistant can SEE
Stats: `stats.grouped` (cluster-aware per-subject accuracies across runs:
t-interval + t-test vs chance)
Not sure which pipeline to build? `catalog.leaderboard()` ranks benchmarked pipelines
per dataset (`meanAccuracyPct` desc, 95% CI) under the canonical `within_session`
protocol — agents pick templates by ranking there and pull the winner with
`catalog.template(pipelineId)`.
## Look at your data first (and at your results)
Before building any pipeline, agents can *see* the data: channels, sampling
rate, trial/class balance, per-channel µV stats, canonical band powers, and a
PSD overview — for a public dataset or a file on disk.
> "Inspect BNCI2014_001 subject S01 before we pick a pipeline."
```python
data.inspect_dataset(dataset="BNCI2014_001", subject="S01", mode="all")
# → {channels: {count: 22, names: [...], flatlined: []}, samplingRate: 250,
# trials: {count: 288, classLabels: [...], classCounts: {...}},
# channelStats: [...], bandPowers: {...}, psd: {...},
# computedFrom: {nSamples: ..., sampleStrategy: "stratified_sample_seed42_4_of_20"}}
```
`subject` is required (e.g. `"S01"`; get the list via `catalog.datasets`) and a
comma-list like `"S01,S03"` loads a cohort; `mode` is `training |
evaluation | all`.
The same works for files: `data.inspect_file` picks the source from the path
shape. An ABSOLUTE path reads the file from disk (local backend only —
desktop app / MCP local mode: no upload step, the data never leaves the
machine); a RELATIVE path — the one `data.upload` returns — describes the
uploaded file on ANY backend (hosted or local):
> "Look at ~/recordings/session-01.edf and tell me if the montage is sane."
```python
data.inspect_file(path="/Users/you/recordings/session-01.edf") # absolute → local file
data.inspect_file(path="uploads/<user>/session-01.edf") # data.upload path → upload
data.inspect_file(path="uploads/<user>/eeg.csv", sampling_rate=250.0) # CSV/TSV/TXT need a rate
```
CSV/TSV/TXT carry no sampling rate, so `sampling_rate` (Hz) is required for
them: without it the tool fails fast with `nimbus.mcp.sampling_rate_required`
before any request is made.
On a hosted backend absolute paths are refused — `data.inspect_file` then returns
guidance (`data.upload` the file and pass the returned path back, or point
the server at a local backend) instead of a dead end.
Why inspect first: **class balance drives stratification** (imbalanced classes
skew accuracy), and **flatlined channels mean a montage/reference problem**
worth fixing before training. And a units caveat: the loader **assumes
volts** — a µV-native CSV reads 1e6x too large; set `unitsScale` (e.g.
`1e-6` with `units: "uV"`) in the pipeline's `custom_data` config when
needed.
The same data — and your results — can be rendered as **PNG figures the
assistant can look at**, not just JSON: `plots.dataset` (PSD / band-power
heatmap / scalp topomap / spectrogram), `plots.erp` (per-class evoked
butterfly — check class structure before training anything),
`plots.confusion` (after a run: which classes are actually confused),
`plots.leaderboard` (CI whisker bars per dataset, BYO submissions
highlighted). `save=true` also writes the PNG into `NIMBUS_EXPORT_DIR/plots/`.
```python
plots.confusion(execution_id="exec_123") # → image + per-class accuracy
plots.dataset(kind="psd", source="dataset", dataset="BNCI2014_001", subject="S01")
stats.grouped(execution_ids=["exec_123", "exec_124"]) # cluster-aware t-interval + p
```
## Uploading data
Bring your own recordings instead of (or alongside) the public datasets.
> "I have a `.edf` recording at `~/recordings/session-01.edf` — upload it and build
> a pipeline around it."
The agent calls `data.upload(file_path=…)`, which registers the file with the
backend and returns the stored path; that path goes into a `custom_data` node's
config (`{"filePath": "<path>", "format": "edf", …}`) for `pipeline.validate` /
`execution.run` / `experiment.run`. For plain CSV/TSV/TXT without embedded
metadata, pass `sampling_rate` (Hz) — the backend silently assumes 250 Hz
otherwise; `format` overrides extension-based detection.
## Experiment campaigns
One `experiment.run` call = a paced sweep of 1-25 pipelines (at most 2 training
runs in flight) with aggregated metrics, instead of the agent babysitting 25
individual `execution.run` polls.
> "Compare CSP-LDA vs EEGNet on BNCI2014_001 across subjects 1-3."
The agent builds six train graphs, calls
`experiment.run(runs=[{name: "csp-lda-s1", train_graph: …}, …])`, gets an
`experimentId` back immediately, then polls `experiment.get(experiment_id)` until
status is `completed` — per-run status and, at the end,
`aggregates` like `{"kappa": {"mean": 0.61, "std": 0.08, "best": {name, value}}}`
(mean/std/best over completed runs only).
## Working with projects
Agent builds, human inspects. Pipelines the agent saves land in real studio
projects, so you can open the canvas and see exactly what ran.
> "Save this pipeline as a project called 'motor-imagery-baseline' — I'll review
> it in the studio."
`project.create(name)` makes the container, `project.save(project_id,
train_graph, expected_revision=…)` writes the graph (layout auto-generated;
pass `expected_revision` from `project.load` so concurrent edits conflict
instead of silently overwriting) and `project.load(project_id)` reads it back
for editing or re-running.
With `MCP_LOCAL_USER_ID` set to your user id, the project shows up in **your**
studio project list.
## Watching a live session
While a streaming session runs, the agent can watch its telemetry and tell you
what it sees.
> "Watch my focus session and tell me when signal quality drops."
The agent polls `stream.telemetry(session_id)` — latest prediction, the recent
window, signal quality (`meanChannelQuality`, `snrDb`, `artifactProbability`) and
running stats — and warns when quality degrades. Each poll also resets the idle
watchdog, so a session under active watch is never auto-stopped; an abandoned one
is shut down after 15 minutes.
## Calibrating a subject
Guided calibration turns a person wearing the device into their own training
data: a cue-guided session records labelled trials, and the agent then trains
the subject's own classifier from the recording.
> "Run a 10-trials-per-class MI calibration on my BrainBit, then train my
> personal classifier when it's done."
The five-tool flow (all non-blocking):
```python
# 1. Start (confirm-gated): fetches the paradigm's calibration template
# (mi / p300 / sart / target_hit), patches in device + trial count, and
# starts the session. The Studio app shows the cues on its calibration
# dashboard automatically.
calibration.start(paradigm="mi", trials_per_class=10, confirm=true)
# → {started: true, executionId: "…"}
# 2. Poll: phase (baseline → imagery trials → complete), current trial, cue,
# progress tally, and an ETA estimate from the timing SSOT.
calibration.status(execution_id="…")
# → {phase: "imagery", currentTrial: {index: 7, total: 20, class: "Left Hand",
# cue: "← LEFT"}, progress: {trialsDone: 6, …}, estimatedRemainingSec: 105.0, …}
# 3./4. Pacing between trials (cues hold; resume anytime).
calibration.pause(execution_id="…") # calibration.resume(...) to continue
# 5. When phase == "complete" the snapshot carries the recording:
# calibration: {uploadId, path, filename, format}. Train from it.
calibration.train(execution_id="…")
# → {trainExecutionId: "…", calibrationUploadId: "…"} → poll execution.get,
# then execution.results for the metrics (kappa, accuracy, …).
```
`mi` trains on the default `mi_headband_csp_lda` template (CAR → 8-30 Hz
bandpass → CSP → LDA) and `p300` on `p300_hardware_xdawn_lda` (xDAWN → LDA on
the recorded flash epochs); `sart` / `target_hit` have no default train
template — pass an explicit `template_id` (from `catalog.templates`) or your
own `train_graph` to `calibration.train`. Either way the first data node is
rewired onto the recorded upload (`custom_data` source pinned to the
`uploadId`), preserving the template's training config.
`calibration.start` refuses to run without `confirm=true` — like `stream.start`,
the device goes on a human's head; call `device.test` first.
Pro plan required — calibration workflows and custom-data training are
freemium-gated (free-tier requests are 403 by the node policy). Local
`X-MCP-Key` principals pass when the desktop app flags the signed-in Pro
session (`MCP_LOCAL_IS_PRO`); otherwise the node policy applies to them too.
The calibrate→train handoff resolves on every backend, desktop-local included:
recordings made with the current build carry upload↔execution linkage in the
desktop store. Recordings from older builds predate that linkage — rerun
`calibration.start` for those (the tool's status hint says the same).
## Safety
`stream.start` refuses to run without `confirm=true` — it connects an EEG device and
starts a live session on a human. The `X-MCP-Key` path is machine-local only
(never accepted on Fly deployments); hosted mode authenticates with a personal
`Authorization: Bearer` token from `nimbus-mcp login` (see **Authentication**
above). Sessions started via MCP are stopped automatically after 15 idle
minutes (see the watchdog note above).
## License
Only this MCP client/wrapper (`nimbus-mcp`) is licensed under the [Apache License 2.0](LICENSE); see [NOTICE](NOTICE).
The Nimbus engine (the `nimbus-bci` SDK), Nimbus Studio, the backend and the hosted services remain
proprietary and are governed by their own terms. Pro features and verified leaderboard submissions
require a paid Nimbus account.
TDQS
Scored across 32 tools
Most tools target distinct actions/resources, but a few pairs overlap: stream_status vs get_live_session both poll a live session, and inspect_dataset vs inspect_file give near-identical exploratory summaries for different sources. Descriptions help disambiguate, but an agent may still hesitate between them.
Names are predominantly consistent snake_case verb_noun (list_nodes, get_template, run_pipeline). Minor deviations like whoami and stream_status break the exact verb_noun pattern but remain readable and conventional.
32 tools is heavy for a single MCP server; several subdomains (streaming, pipelines, datasets, projects, artifacts) could be consolidated. While each tool has a purpose, the set exceeds the 25-tool threshold where navigation becomes cumbersome.
The surface covers major workflows: device streaming, pipeline building/validation/execution, experiments, datasets, projects, and artifacts. Minor gaps exist (no delete/update for projects, pipelines, or artifacts), but agents can work around them.