AI agents build, train, and analyze BCI/EEG pipelines: data, models, experiments, live sessions.
io.github.nimbusbci/nimbus-mcp (MCP Server)
The MCP server io.github.nimbusbci/nimbus-mcp supports AI agents for building, training, and analyzing BCI/EEG pipelines. It focuses on workflows that cover data, models, experiments, and live sessions, as described by the server description.
๐ ๏ธ Key Features
Builds BCI/EEG pipelines
Trains BCI/EEG models
Analyzes BCI/EEG pipelines
Covers data, models, experiments, and live sessions
๐ Use Cases
BCI/EEG pipeline development
Model training for BCI/EEG workflows
Experiment management for BCI/EEG research
Running and analyzing live BCI/EEG sessions
โก Developer Benefits
Clear scope around BCI/EEG pipeline lifecycle
Workflow categories explicitly include data, models, experiments, and live sessions
โ ๏ธ Limitations
No additional details (e.g., tool count, topics, or capabilities beyond the description) are provided in the source data.
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.
List Nimbus pipeline node types (data, preprocessing, features, models...).
Use catalog.node_schema(node_type) for one node's full config schema and ports.
Parameters1
category
any
optional
Optional filter โ e.g. "data", "preprocessing", "features",
"models" (exact category ids from the unfiltered list).
Curated public EEG datasets (MOABB packs) available to pipelines.
Parameters1
only_on_disk
boolean
optional
Only return datasets whose data packs are present on this
backend (True by default; False also lists known-but-missing sets).
Raw schema
{
"type": "object",
"properties": {
"only_on_disk": {
"default": true,
"type": "boolean",
"description": "Only return datasets whose data packs are present on this\nbackend (True by default; False also lists known-but-missing sets)."
}
},
"additionalProperties": false
}
catalog.leaderboard
Public benchmark leaderboard: pipeline rankings per dataset.
Rankings are per-dataset under the canonical ``within_session`` protocol
(see ``protocol``). Within each dataset, ``rows`` are sorted desc by
``meanAccuracyPct`` (95% CI in ``ciLoPct``/``ciHiPct``). Use ``pipelineId``
as the template id hint for ``catalog.template`` when building a pipeline.
``updated`` marks each dataset's most recent run; ``packFingerprint``
identifies the exact dataset pack the scores came from.
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().
Validate one node's config object against its schema (catalog.node_schema).
Parameters2
node_type
string
required
Node type id from catalog.nodes (e.g. "csp", "nimbus_lda").
config
object
required
The node's config object to check.
Raw schema
{
"type": "object",
"properties": {
"node_type": {
"type": "string",
"description": "Node type id from catalog.nodes (e.g. \"csp\", \"nimbus_lda\")."
},
"config": {
"additionalProperties": true,
"type": "object",
"description": "The node's config object to check."
}
},
"required": [
"node_type",
"config"
],
"additionalProperties": false
}
execution.run
Start a pipeline run (NON-BLOCKING). Returns executionId โ poll with
execution.get() until status is completed/failed, then execution.results().
Parameters5
train_graph
object
required
Pipeline graph {nodes: [{id, type, config}], connections:
[{from, to}]} as built by catalog.template/pipeline.validate.
name
any
optional
Display name for the run (shown in the Runs list).
description
any
optional
Optional longer description of the experiment.
subject
any
optional
Optional dataset subject code (e.g. "S01") recorded with the run.
layout
any
optional
Optional canvas positions {nodes: {id: {x, y}}}; a deterministic
grid is synthesized when omitted.
Raw schema
{
"type": "object",
"properties": {
"train_graph": {
"additionalProperties": true,
"type": "object",
"description": "Pipeline graph {nodes: [{id, type, config}], connections:\n[{from, to}]} as built by catalog.template/pipeline.validate."
},
"name": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Display name for the run (shown in the Runs list)."
},
"description": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Optional longer description of the experiment."
},
"subject": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Optional dataset subject code (e.g. \"S01\") recorded with the run."
},
"layout": {
"anyOf": [
{
"additionalProperties": true,
"type": "object"
},
{
"type": "null"
}
],
"default": null,
"description": "Optional canvas positions {nodes: {id: {x, y}}}; a deterministic\ngrid is synthesized when omitted."
}
},
"required": [
"train_graph"
],
"additionalProperties": false
}
execution.cancel
Cancel a running execution.
Parameters1
execution_id
string
required
The run to terminate (from execution.run/execution.list).
Raw schema
{
"type": "object",
"properties": {
"execution_id": {
"type": "string",
"description": "The run to terminate (from execution.run/execution.list)."
}
},
"required": [
"execution_id"
],
"additionalProperties": false
}
execution.get
Execution status summary (status: running/completed/failed/cancelled).
Parameters1
execution_id
string
required
The run to check (from execution.run/execution.list).
Raw schema
{
"type": "object",
"properties": {
"execution_id": {
"type": "string",
"description": "The run to check (from execution.run/execution.list)."
}
},
"required": [
"execution_id"
],
"additionalProperties": false
}
execution.list
Recent executions. Optional status filter (running/completed/failed/cancelled).
Parameters2
limit
integer
optional
Maximum number of runs to return (default 20).
status
any
optional
Filter by run status: running/completed/failed/cancelled.
Raw schema
{
"type": "object",
"properties": {
"limit": {
"default": 20,
"type": "integer",
"description": "Maximum number of runs to return (default 20)."
},
"status": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Filter by run status: running/completed/failed/cancelled."
}
},
"additionalProperties": false
}
execution.results
Metrics for a completed run. Trimmed by default (accuracy, kappa, ITR,
confusion matrix, per-class); full=True returns the complete result object.
Parameters2
execution_id
string
required
The completed run to fetch metrics for.
full
boolean
optional
Return the backend's complete result object (all fields).
Raw schema
{
"type": "object",
"properties": {
"execution_id": {
"type": "string",
"description": "The completed run to fetch metrics for."
},
"full": {
"default": false,
"type": "boolean",
"description": "Return the backend's complete result object (all fields)."
}
},
"required": [
"execution_id"
],
"additionalProperties": false
}
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 get 403 by policy.
Parameters14
paradigm
string
optional
mi | p300 | sart | target_hit.
confirm
boolean
optional
MUST be true โ explicit user go-ahead for a session on their head.
trials_per_class
any
optional
Override the template's trial count (e.g. 3 for smoke tests).
classes
any
optional
Override class list [{id,label,cue}] (MI default: left/right hand).
name
any
optional
Display name recorded on the execution.
device_type
any
optional
Device id from device.list (omit โ template default synthetic).
connection_type
any
optional
Device selector when several exist (e.g. serial vs wifi).
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.
Parameters1
execution_id
string
required
The calibration run to inspect (from calibration.start).
Raw schema
{
"type": "object",
"properties": {
"execution_id": {
"type": "string",
"description": "The calibration run to inspect (from calibration.start)."
}
},
"required": [
"execution_id"
],
"additionalProperties": false
}
calibration.pause
Pause a running calibration between trials (cues hold; resume anytime).
Parameters1
execution_id
string
required
The calibration run to pause.
Raw schema
{
"type": "object",
"properties": {
"execution_id": {
"type": "string",
"description": "The calibration run to pause."
}
},
"required": [
"execution_id"
],
"additionalProperties": false
}
calibration.resume
Resume a paused calibration session.
Parameters1
execution_id
string
required
The calibration run to resume.
Raw schema
{
"type": "object",
"properties": {
"execution_id": {
"type": "string",
"description": "The calibration run to resume."
}
},
"required": [
"execution_id"
],
"additionalProperties": false
}
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 requires a Postgres-backed backend (hosted or local dev); a
desktop-local session completes and records, but its upload can't be
resolved by MCP train today.
Parameters5
execution_id
string
required
The COMPLETED calibration run (from calibration.start).
paradigm
string
optional
Paradigm of the recording (mi | p300 | sart | target_hit);
only mi has a default train template.
template_id
any
optional
Train template to use (e.g. from catalog.templates);
required for non-mi paradigms (mi defaults to mi_headband_csp_lda).
train_graph
any
optional
Explicit train graph instead of a template; its first
data node (custom_data/public_data) is rewired onto the recording.
name
any
optional
Display name recorded on the training run.
Raw schema
{
"type": "object",
"properties": {
"execution_id": {
"type": "string",
"description": "The COMPLETED calibration run (from calibration.start)."
},
"paradigm": {
"default": "mi",
"type": "string",
"description": "Paradigm of the recording (mi | p300 | sart | target_hit);\nonly mi has a default train template."
},
"template_id": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Train template to use (e.g. from catalog.templates);\nrequired for non-mi paradigms (mi defaults to mi_headband_csp_lda)."
},
"train_graph": {
"anyOf": [
{
"additionalProperties": true,
"type": "object"
},
{
"type": "null"
}
],
"default": null,
"description": "Explicit train graph instead of a template; its first\ndata node (custom_data/public_data) is rewired onto the recording."
},
"name": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Display name recorded on the training run."
}
},
"required": [
"execution_id"
],
"additionalProperties": false
}
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.
Parameters2
runs
array
required
1-25 entries, each {name: str, train_graph: {nodes, connections}}
(same graph shape as execution.run's train_graph).
max_concurrent
integer
optional
Parallel submissions cap, clamped to 1-2 (default 2).
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.
Parameters14
device_type
string
required
Device id from device.list (e.g. "brainbit").
confirm
boolean
optional
MUST be true to start โ the explicit user go-ahead for a live
session on their head; anything else is refused with zero requests.
session_id
any
optional
Optional existing session to resume/reuse.
chunk_size
integer
optional
Samples per streamed chunk (default 125).
n_channels
integer
optional
Channel count to open (default 8).
connection_type
any
optional
Device-specific selector (e.g. serial vs wifi).
port
any
optional
Serial/COM port for wired devices.
ip_address
any
optional
Device IP for network/wifi devices.
ip_port
any
optional
Port for network devices.
stream_name
any
optional
LSL stream name (LSL devices).
source_id
any
optional
LSL source id.
mac_address
any
optional
Bluetooth MAC (BT devices).
serial_number
any
optional
Device serial (some BLE stacks).
idle_timeout_sec
integer
optional
Watchdog: stop+disconnect after this many seconds
without a status poll (default 900; 0 disables).
Raw schema
{
"type": "object",
"properties": {
"device_type": {
"type": "string",
"description": "Device id from device.list (e.g. \"brainbit\")."
},
"confirm": {
"default": false,
"type": "boolean",
"description": "MUST be true to start โ the explicit user go-ahead for a live\nsession on their head; anything else is refused with zero requests."
},
"session_id": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Optional existing session to resume/reuse."
},
"chunk_size": {
"default": 125,
"type": "integer",
"description": "Samples per streamed chunk (default 125)."
},
"n_channels": {
"default": 8,
"type": "integer",
"description": "Channel count to open (default 8)."
},
"connection_type": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Device-specific selector (e.g. serial vs wifi)."
},
"port": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Serial/COM port for wired devices."
},
"ip_address": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Device IP for network/wifi devices."
},
"ip_port": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"description": "Port for network devices."
},
"stream_name": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "LSL stream name (LSL devices)."
},
"source_id": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "LSL source id."
},
"mac_address": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Bluetooth MAC (BT devices)."
},
"serial_number": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Device serial (some BLE stacks)."
},
"idle_timeout_sec": {
"default": 900,
"type": "integer",
"description": "Watchdog: stop+disconnect after this many seconds\nwithout a status poll (default 900; 0 disables)."
}
},
"required": [
"device_type"
],
"additionalProperties": false
}
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).
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.
Live snapshot of a streaming session: latest prediction + recent window,
signal quality (meanChannelQuality, snrDb, artifactProbability), indicators,
running stats. Poll this while a session runs. Live telemetry requires a
DEPLOYED model session (hub deploy / playback with a classifier); modelless
hardware streams have no telemetry โ use stream.status for those. 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.
Parameters2
session_id
string
required
The streaming session to read telemetry for.
window
integer
optional
How many recent predictions/chunks to include (default 50).
Raw schema
{
"type": "object",
"properties": {
"session_id": {
"type": "string",
"description": "The streaming session to read telemetry for."
},
"window": {
"default": 50,
"type": "integer",
"description": "How many recent predictions/chunks to include (default 50)."
}
},
"required": [
"session_id"
],
"additionalProperties": false
}
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 โ 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).
Parameters4
file_path
string
required
Local file to upload (.edf/.bdf/.mat/.csv/.txt/.tsv/.h5/.hdf5, <=500MB).
dataset_name
any
optional
Optional label for the uploaded dataset.
sampling_rate
any
optional
Hz for headerless CSV/TSV/TXT (REQUIRED there, e.g. 250.0).
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.
Parameters3
dataset
string
required
Dataset id from catalog.datasets (e.g. "BNCI2014_001").
subject
any
optional
REQUIRED subject code ("S01") or comma-list cohort ("S01,S03").
mode
string
optional
Which split to summarize โ training | evaluation | all.
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).
Parameters1
path
string
required
ABSOLUTE filesystem path (local backends only) or the RELATIVE
upload path returned by data.upload (works on any backend).
Raw schema
{
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "ABSOLUTE filesystem path (local backends only) or the RELATIVE\nupload path returned by data.upload (works on any backend)."
}
},
"required": [
"path"
],
"additionalProperties": false
}
project.create
Create a project (container for one pipeline document). Returns projectId.
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, watch live EEG
sessions, and (explicitly confirmed) start live streaming โ through your local
Nimbus backend or the hosted deployment, with one nimbus-mcp login.
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:
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
bash
pip install nimbus-mcp # or: uvx nimbus-mcp
(Also installable from the repo: pip install -e nimbus-studio/mcp.)
Zero-hardware demo: 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:
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:
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.
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.
No credential anywhere? The server still starts โ in setup mode. Every
tool call returns {ok: false, setupRequired: true, message, options} with the
three paths above, so your agent walks you through onboarding instead of the
server crashing. A hosted token rejected mid-session (expired or revoked)
returns the same shape, including how many days ago it expired and a
nimbus-mcp login first option.
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.
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 extrasexport 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.
Env vars: NIMBUS_API_URL (default http://127.0.0.1:8080, or the store's
api_url after a login), 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).
Hosted gateway (streamable HTTP)
nimbus-mcp serve runs the same 48 tools over streamable HTTP instead of
stdio โ for remote MCP clients, registries (Smithery lists URL-based
servers), and browser-side clients:
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 API token
arrives as a header, so one shared deployment serves many users as
themselves. Calls without a header get setup guidance (add the header, or
install locally via uvx).
json
{"mcpServers":{"nimbus":{"type":"http","url":"https://nimbus-mcp.fly.dev/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.)
Claude Code
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
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."
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."
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/.
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) writes the graph (layout auto-generated, revision conflicts retried
once) 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); p300 / 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 requires a Postgres-backed backend (hosted or local
dev): a desktop-local session completes and records, but its upload can't be
resolved by MCP train today.
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).
Nimbus backend URL. Defaults to the local backend (http://127.0.0.1:8080) โ the desktop app and local key flow expect this; use https://nimbus-studio.fly.dev to run against the hosted API.
NIMBUS_TOKENsecret
Hosted API token (nimb_โฆ) minted in Nimbus Studio โ Account โ API tokens. Not needed if you already ran `nimbus-mcp login`.
NIMBUS_TOKEN_FILE
Path to a 0600 JSON file {"token": "โฆ"} holding a hosted API token (keeps secrets out of MCP configs).
NIMBUS_MCP_KEYsecret
Local-backend key; must match MCP_LOCAL_KEY on your Nimbus backend (dev server) or the desktop app's key.
NIMBUS_MCP_KEY_FILE
Path to a local-backend key file. The desktop app's mcp-key.json is auto-discovered without this.