nimbus-mcp
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 pipelinesinto studio projects, run multi-configuration experiment campaigns, watch live EEGsessions, and (explicitly confirmed) start live streaming — through your localNimbus backend or the hosted deployment, with one nimbus-mcp login.
Docs: docs.nimbusbci.com · Overview: nimbusbci.com/mcp · Jump to Quickstart
Quickstart (hosted, recommended)
Nothing to install: point your MCP client at the hosted serverhttps://nimbus-mcp.fly.dev/mcp. On first use your client opens a browsersign-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):
{
"mcpServers": {
"nimbus": { "url": "https://nimbus-mcp.fly.dev/mcp" }
}
}
Claude Code:
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 apaid Claude plan.)
Other clients (streamable HTTP):
{
"mcpServers": {
"nimbus": { "type": "http", "url": "https://nimbus-mcp.fly.dev/mcp" }
}
}
Headless / CI (no browser): OAuth stays the primary path, but thegateway also accepts a Nimbus API key (create one on the Studio Account page)as a Bearer header:
{
"mcpServers": {
"nimbus": {
"type": "http",
"url": "https://nimbus-mcp.fly.dev/mcp",
"headers": { "Authorization": "Bearer ${NIMBUS_TOKEN}" }
}
}
}
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 showexecution.results (accuracy / kappa), a plots.* figure and thecatalog.leaderboard comparison — all on public MOABB data, no EEG deviceneeded. The longer guided loop is in examples/demo.md.
The server also ships 5 ready-made MCP prompts (your client lists them asslash commands / prompt templates): mi_csp_lda_bci_iv_2a,compare_mi_pipelines, foundation_backbones, inspect_my_eeg andleaderboard_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.* |
— | — | ✅ | ✅ |
| Live hardware sessions on a human | ✅ confirm-gated | ✅ | ✅ | — | — |
| 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 — parity study, 3 datasets / 6 pipelines / 2 protocols | band-ratio heuristics | — | — | — |
Scope honesty (deliberate non-goals): no stimulation output (eeg-mcp'striple-interlocked send_stim_event is genuinely novel; we don't drivestimulators), no spike/ephys data (ephys-mcp covers intracorticalrecording — different data plane), no source localization / full MNEparity (mne-mcp is a deep offline MNE wrapper; plots.* covers thefigures agents need on Nimbus data).
Install locally (stdio)
Prefer running the server on your own machine (or using your localStudio desktop backend)? Sign in once, then use a secret-free config:
uvx nimbus-mcp login # or: pip install nimbus-mcp && nimbus-mcp login
{
"mcpServers": {
"nimbus": { "command": "uvx", "args": ["nimbus-mcp"] }
}
}
Both uvx nimbus-mcp and python -m nimbus_mcp (inside the env where it is installed) start thestdio MCP server. Agent-oriented install steps (e.g. for Cline): llms-install.md.
(Also installable from the repo: pip install -e nimbus-studio/mcp.)
Zero-hardware demo: examples/demo.md — a copy-pasteagent 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:
nimbus-mcp login(recommended — hosted API, no token pasting). Adevice-code login: the command prints a URL and an 8-character code, opensyour browser, you approve in Nimbus Studio, and the minted token is storedat~/.nimbus/credentials.json(0600) and picked up automatically on everyfuture start.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 credentialAfter a login, MCP client configs need no secret at all:
{ "mcpServers": { "nimbus": { "command": "uvx", "args": ["nimbus-mcp"] } } }Desktop app — zero config. Just have the Nimbus Studio desktop apprunning: 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 localbackend athttp://127.0.0.1:8080. Nothing to paste or configure.Environment variables (advanced / CI).
NIMBUS_TOKEN(a hosted APItokennimb_…minted in Nimbus Studio → Account → API tokens) orNIMBUS_TOKEN_FILE(a 0600 JSON file{"token": "…"}— keeps the secretout of process env and MCP configs), or the local pairNIMBUS_MCP_KEY/NIMBUS_MCP_KEY_FILE(must matchMCP_LOCAL_KEYon alocal backend — see below). Explicit env always beats files on disk.ANIMBUS_TOKEN/NIMBUS_TOKEN_FILEtoken withNIMBUS_API_URLunsettalks to the hosted API (https://nimbus-studio.fly.dev); setNIMBUS_API_URLto point it anywhere else.
No credential anywhere? That is the Local Free tier — it works. Theserver starts anonymously against the local backend (desktop app or devbackend on http://127.0.0.1:8080): templates, your own data, localexecution/plots/stats, BIDS export and local-only storage need no NimbusStudio login at all. Login is requested only at first cloud value — atool that needs an identity (leaderboard submission, verified/cross-machineexperiment 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 alocal call fails because no local backend/identity exists, the response isthe local-mode variant of {ok: false, setupRequired: true, message, options} (start the desktop app / set a dev key — still no login). Ahosted token rejected mid-session (expired or revoked) returns the classicsetup 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-runquota, and (in token mode) the token's days-to-expiry; nimbus-mcp status isthe terminal equivalent with a live backend probe.
What a hosted token means
- The token IS you. Requests run under your account: executions appear in yourstudio history and your plan's quotas and limits apply — there is no separateagent allowance. When the free monthly quota is exhausted, run errors carry theupgrade 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 aresnapshotted at mint time — after an upgrade, re-login (or revoke and re-createthe token) to pick up the new plan. An expired token surfaces as setupguidance 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 or365 days, and pick its scopes. CI keys are a Pro feature (Pro plan orPioneer 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 onPOST /api/me/api-tokens). Personal tokens keep the old default: no scopeselected = full access, and every token minted before scopes existed keepsworking unchanged. The backend enforces scopes on every request; a missingscope answers 403 nimbus.auth.token_scope_insufficient. account.whoami andnimbus-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_URLneeded — a token defaults to hosted Nimbus). GitHub Actions example:
- 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 headerAuthorization: Bearer $NIMBUS_TOKEN. Rotate before expiry (the Account pageflags 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 yourcurrent Clerk Billing plan (cached for about a minute), so cancelling Proremoves 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 everyrequest with
403 nimbus.auth.ci_key_requires_pro. Resubscribing makes itwork again (no need to re-mint); otherwise revoke it and switch to apersonal token. - You can have at most 20 active (unrevoked, unexpired) API tokens;minting one more answers
409 nimbus.me.api_token_limit_reached. Tokenscreated 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 anderror codes, duration buckets — never your data, file paths, or code) isopt-in: you are asked once in the first tool response of a freshinstall, and nimbus-mcp telemetry on|off|status (orNIMBUS_MCP_TELEMETRY=on|off) controls it anytime. With telemetry offthere are zero telemetry network calls, and everything local keepsworking. Full statement: PRIVACY.md. A quiet PyPI versioncheck at startup (cached 24 h, silent when current or offline) adds aone-time versionNotice to a tool response when your client is behindthe 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) withDEBUG=1. - The backend started with
MCP_LOCAL_KEY=<some-secret>(never set this on Fly — it isrefused there). - Desktop app users: open Settings → MCP & Agents — no manual key setup (the appcreates the key, injects it into its backend, and hands you copy-ready configs).
No desktop app required. Acquisition is fully in-backend: a headless localbackend + nimbus-mcp can drive real hardware (BrainFlow boards, BrainBit overBLE, LSL, PiEEG) and synthetic boards, with zero Electron:
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 atrained model (modelless streams report live signal quality + focus/relaxationindicators).
Configure the backend
Desktop/dev env (e.g. backend-py/data/.env or the dev shell):
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 yourown Clerk user id (user_…) so everything the agent creates — projects, savedpipelines, executions — appears in your studio UI as yours. Pick one owner and stickwith it: switching the id mid-life splits ownership of agent-created work across twoprincipals, and neither identity then sees the whole history.
Watchdog default: streaming sessions started through MCP are auto-stopped after15 minutes with no one watching (every stream.status / stream.telemetry pollresets the timer). Pass idle_timeout_sec=0 to stream.start to disable it for asession.
When enabling MCP_LOCAL_KEY on a machine connected to an untrusted network, also setHOST=127.0.0.1 on the backend. The 0.0.0.0 default (settings.host) applies to thebare dev server (python -m nimbus_backend.server.app), so with it the key wouldotherwise be accepted from the LAN; backend-py/scripts/run_server.py already defaultsto 127.0.0.1, and the desktop app pins loopback itself.
Run the server
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'sapi_url after a login, https://nimbus-studio.fly.dev for aNIMBUS_TOKEN / NIMBUS_TOKEN_FILE token, else http://127.0.0.1:8080), NIMBUS_TOKEN / NIMBUS_TOKEN_FILE (hosted APItoken — see Authentication), NIMBUS_MCP_KEY (must match MCP_LOCAL_KEY),NIMBUS_MCP_KEY_FILE (path to a 0600 JSON file {"key": "…"} — the desktopapp's one-click MCP setup writes it; consulted only when NIMBUS_MCP_KEY isunset), NIMBUS_EXPORT_DIR (default ~/nimbus-exports). With none of thetoken/key vars set, the login store and then the desktop key file areauto-discovered; with nothing found, the server runs in setup mode (every toolreturns onboarding guidance).
Self-hosting the gateway (streamable HTTP)
The public gateway https://nimbus-mcp.fly.dev/mcp uses OAuth sign-in forinteractive clients and also accepts Authorization: Bearer <API key> forheadless/CI use — see Quickstart. Thissection is for running your own.
nimbus-mcp serve runs the same 63 tools over streamable HTTP instead ofstdio — for remote MCP clients, registries (Smithery lists URL-basedservers), and browser-side clients:
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 (thegateway itself holds no credential to gate access with).
The gateway holds no credential itself: each request's Nimbus tokenarrives per request, so one shared deployment serves many users asthemselves. With OAuth on (below, and on the public gateway) clients sign inin the browser. On a self-hosted gateway without OAuth, send the token asan X-Nimbus-Token or Authorization: Bearer nimb_… header; headerless callsthen get setup guidance:
{
"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 thefallback 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 authorizationserver (MCP authorization spec), so clients like Claude and Cursor can connectwith just the URL — no pasted token:
/.well-known/oauth-protected-resource/mcpand/.well-known/oauth-authorization-servermetadata;- 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;/tokenexchanges the code for a normalnimb_token (30 days, norefresh token; scopesread,run,write,leaderboard:submit, or fullaccess when all/none are requested). Re-authorizing the same clientrevokes that client's previous token, and every OAuth token is listed (andrevocable) under Account → API tokens.
When the flag is on, requests with no token get the spec 401 with aWWW-Authenticate: Bearer resource_metadata=… header (that is what starts theclient's sign-in), so headerless tools/list is no longer public. ExistingX-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 thegateway app, and the same MCP_OAUTH_GATEWAY_SECRET on the backend. With thesecret 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.This form runs a dev checkout against a local backend with MCP_LOCAL_KEY:
# --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 — seeQuickstart andInstall locally.
{
"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.leaderboardData: 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_nodeRun: 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.exportLive: device.list, device.test, stream.start (needs confirm=true),stream.status, stream.telemetry, stream.stopCalibration: calibration.start (needs confirm=true), calibration.status,calibration.pause, calibration.resume, calibration.trainNeurofeedback protocols: protocol.validate/protocol.save (versioned specs),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.savenor protocol.start_assignment accepts a name argument (it was neverpersisted — 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 (CIwhisker bars, BYO highlighted; track=pretrained_few_trial overlays theclassical LOSO baseline side-by-side) — PNG figures the assistant can SEEStats: 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 pipelinesper dataset (meanAccuracyPct desc, 95% CI) under the canonical within_sessionprotocol — agents pick templates by ranking there and pull the winner withcatalog.template(pipelineId).
Look at your data first (and at your results)
Before building any pipeline, agents can see the data: channels, samplingrate, trial/class balance, per-channel µV stats, canonical band powers, and aPSD overview — for a public dataset or a file on disk.
"Inspect BNCI2014_001 subject S01 before we pick a pipeline."
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 acomma-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 pathshape. An ABSOLUTE path reads the file from disk (local backend only —desktop app / MCP local mode: no upload step, the data never leaves themachine); a RELATIVE path — the one data.upload returns — describes theuploaded file on ANY backend (hosted or local):
"Look at ~/recordings/session-01.edf and tell me if the montage is sane."
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 forthem: without it the tool fails fast with nimbus.mcp.sampling_rate_requiredbefore any request is made.
On a hosted backend absolute paths are refused — data.inspect_file then returnsguidance (data.upload the file and pass the returned path back, or pointthe server at a local backend) instead of a dead end.
Why inspect first: class balance drives stratification (imbalanced classesskew accuracy), and flatlined channels mean a montage/reference problemworth fixing before training. And a units caveat: the loader assumesvolts — a µV-native CSV reads 1e6x too large; set unitsScale (e.g.1e-6 with units: "uV") in the pipeline's custom_data config whenneeded.
The same data — and your results — can be rendered as PNG figures theassistant can look at, not just JSON: plots.dataset (PSD / band-powerheatmap / scalp topomap / spectrogram), plots.erp (per-class evokedbutterfly — check class structure before training anything),plots.confusion (after a run: which classes are actually confused),plots.leaderboard (CI whisker bars per dataset, BYO submissionshighlighted). save=true also writes the PNG into NIMBUS_EXPORT_DIR/plots/.
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
.edfrecording at~/recordings/session-01.edf— upload it and builda pipeline around it."
The agent calls data.upload(file_path=…), which registers the file with thebackend and returns the stored path; that path goes into a custom_data node'sconfig ({"filePath": "<path>", "format": "edf", …}) for pipeline.validate /execution.run / experiment.run. For plain CSV/TSV/TXT without embeddedmetadata, pass sampling_rate (Hz) — the backend silently assumes 250 Hzotherwise; format overrides extension-based detection.
Experiment campaigns
One experiment.run call = a paced sweep of 1-25 pipelines (at most 2 trainingruns in flight) with aggregated metrics, instead of the agent babysitting 25individual execution.run polls.
"Compare CSP-LDA vs EEGNet on BNCI2014_001 across subjects 1-3."
The agent builds six train graphs, callsexperiment.run(runs=[{name: "csp-lda-s1", train_graph: …}, …]), gets anexperimentId back immediately, then polls experiment.get(experiment_id) untilstatus 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 studioprojects, so you can open the canvas and see exactly what ran.
"Save this pipeline as a project called 'motor-imagery-baseline' — I'll reviewit 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 conflictinstead of silently overwriting) and project.load(project_id) reads it backfor editing or re-running.With MCP_LOCAL_USER_ID set to your user id, the project shows up in yourstudio project list.
Watching a live session
While a streaming session runs, the agent can watch its telemetry and tell youwhat it sees.
"Watch my focus session and tell me when signal quality drops."
The agent polls stream.telemetry(session_id) — latest prediction, the recentwindow, signal quality (meanChannelQuality, snrDb, artifactProbability) andrunning stats — and warns when quality degrades. Each poll also resets the idlewatchdog, so a session under active watch is never auto-stopped; an abandoned oneis shut down after 15 minutes.
Calibrating a subject
Guided calibration turns a person wearing the device into their own trainingdata: a cue-guided session records labelled trials, and the agent then trainsthe subject's own classifier from the recording.
"Run a 10-trials-per-class MI calibration on my BrainBit, then train mypersonal classifier when it's done."
The five-tool flow (all non-blocking):
# 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 Hzbandpass → CSP → LDA) and p300 on p300_hardware_xdawn_lda (xDAWN → LDA onthe recorded flash epochs); sart / target_hit have no default traintemplate — pass an explicit template_id (from catalog.templates) or yourown train_graph to calibration.train. Either way the first data node isrewired onto the recorded upload (custom_data source pinned to theuploadId), 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 arefreemium-gated (free-tier requests are 403 by the node policy). LocalX-MCP-Key principals pass when the desktop app flags the signed-in Prosession (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 thedesktop store. Recordings from older builds predate that linkage — reruncalibration.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 andstarts 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 personalAuthorization: Bearer token from nimbus-mcp login (see Authenticationabove). Sessions started via MCP are stopped automatically after 15 idleminutes (see the watchdog note above).
License
Only this MCP client/wrapper (nimbus-mcp) is licensed under the Apache License 2.0; see NOTICE.The Nimbus engine (the nimbus-bci SDK), Nimbus Studio, the backend and the hosted services remainproprietary and are governed by their own terms. Pro features and verified leaderboard submissionsrequire a paid Nimbus account.