Skip to main content
Glama

nimbus-mcp

Add to Cursor Install in VS Code M8ven Verified

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 · Overview: nimbusbci.com/mcp · Jump to Quickstart

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):

{
  "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 a paid 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 the gateway 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 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.

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.

Related MCP server: neuro-cube-brain

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 — 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:

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 the stdio 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-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.

    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:

    {
      "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:

- 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. 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:

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):

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

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. 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:

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:

{
  "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. 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 — see Quickstart and Install 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.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."

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."

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/.

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):

# 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; see 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.

Available Tools

32 tools
cancel_executionCancel ExecutionCInspect

Cancel a running execution.

Inputs: execution_id (required). Returns: object

create_projectCreate ProjectBInspect

Create a project (container for one pipeline document). Returns projectId.

Inputs: name (required), description. Returns: object

download_artifactDownload ArtifactBInspect

Download one artifact file to NIMBUS_EXPORT_DIR/executions// and return its path.

Inputs: execution_id (required), artifact_name (required). Returns: object

export_pythonExport PythonCInspect

Export the pipeline as a standalone runnable Python bundle (zip saved locally).

Inputs: train_graph (required), name. Returns: object

get_executionGet ExecutionBInspect

Execution status summary (status: running/completed/failed/cancelled).

Inputs: execution_id (required). Returns: object

get_experimentGet ExperimentBInspect

Experiment snapshot: status (running/completed/failed), per-run rows ({name, executionId, status, error?, metrics?}) and, once finished, aggregates {metric: {mean, std, best: {name, value}}} over completed runs only (std = population; None below 2 values).

Inputs: experiment_id (required). Returns: object

get_leaderboardGet LeaderboardAInspect

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 get_template when building a pipeline. updated marks each dataset's most recent run; packFingerprint identifies the exact dataset pack the scores came from.

Inputs: no named parameters. Returns: object

get_live_sessionGet Live SessionAInspect

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 start_stream's idle_timeout_sec), keeping an actively watched session alive.

Inputs: session_id (required), window. Returns: object

get_node_schemaGet Node SchemaBInspect

Full config JSON schema + input/output ports for one node type.

Inputs: node_type (required). Returns: object

get_resultsGet ResultsAInspect

Metrics for a completed run. Trimmed by default (accuracy, kappa, ITR, confusion matrix, per-class); full=True returns the complete result object.

Inputs: execution_id (required), full. Returns: object

get_templateGet TemplateBInspect

Full template incl. the 'train' execGraph needed by run_pipeline/validate_pipeline.

Inputs: template_id (required). Returns: object

inspect_datasetInspect DatasetAInspect

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 list_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.

Inputs: dataset (required), mode, subject. Returns: object

inspect_fileInspect FileAInspect

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 upload_data 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).

Inputs: path (required). Returns: object

list_artifactsList ArtifactsCInspect

Trained artifacts (models/filters, e.g. *.pkl) saved by an execution.

Inputs: execution_id (required). Returns: object

list_datasetsList DatasetsCInspect

Curated public EEG datasets (MOABB packs) available to pipelines.

Inputs: only_on_disk. Returns: object

list_devicesList DevicesAInspect

EEG devices supported by this backend (OpenBCI, Muse, BrainBit, LSL, PiEEG...).

Inputs: no named parameters. Returns: object

list_executionsList ExecutionsCInspect

Recent executions. Optional status filter (running/completed/failed/cancelled).

Inputs: limit, status. Returns: object

list_nodesList NodesAInspect

List Nimbus pipeline node types (data, preprocessing, features, models...).

Use get_node_schema(node_type) for one node's full config schema and ports.

Inputs: category. Returns: object

list_projectsList ProjectsBInspect

List projects owned by the current principal (agent work included).

Inputs: no named parameters. Returns: object

list_templatesList TemplatesAInspect

List built-in starter pipelines (MI/P300/SSVEP...). get_template(id) for the graph.

Inputs: no named parameters. Returns: object

load_pipelineLoad PipelineBInspect

Load a project's saved pipeline (train graph + meta) for editing/re-running.

Inputs: project_id (required). Returns: object

run_experimentRun ExperimentAInspect

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 get_experiment() for per-run status and, once finished, aggregated metrics.

Inputs: runs (required), max_concurrent. Returns: object

run_pipelineRun PipelineAInspect

Start a pipeline run (NON-BLOCKING). Returns executionId — poll with get_execution() until status is completed/failed, then get_results(). layout is optional canvas positions ({nodes: {id: {x, y}}}); a grid is synthesized when omitted.

Inputs: train_graph (required), name, layout, subject, +1 more. Returns: object

save_pipelineSave PipelineBInspect

Save a pipeline graph into a project (visible on the studio canvas). Handles revision conflicts automatically (one retry).

Inputs: project_id (required), train_graph (required), name, subject. Returns: object

start_streamStart StreamAInspect

Connect an EEG device and START a live streaming session on the user's head. Requires confirm=True; call test_device first. Track with stream_status(). Idle watchdog: if no stream_status()/get_live_session() 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.

Inputs: device_type (required), port, confirm, ip_port, +10 more. Returns: object

stop_streamStop StreamAInspect

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.

Inputs: session_id (required). Returns: object

stream_statusStream StatusAInspect

Live snapshot of a streaming session (running, deviceConnected). Polling this also feeds the idle watchdog: each call resets the session's idle timer (see start_stream's idle_timeout_sec).

Inputs: session_id (required). Returns: object

test_deviceTest DeviceAInspect

Test a device connection WITHOUT starting a stream (safe, no confirm needed).

Inputs: device_type (required), port, ip_port, source_id, +5 more. Returns: object

upload_dataUpload DataAInspect

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).

Inputs: file_path (required), format, dataset_name, sampling_rate. Returns: object

validate_node_configValidate Node ConfigBInspect

Validate one node's config object against its schema (get_node_schema).

Inputs: config (required), node_type (required). Returns: object

validate_pipelineValidate PipelineAInspect

Validate a pipeline graph before running. ExecGraphSnapshot: {nodes: [{id, type, config}], connections: [{from, to}]}. Build it from get_template(id).train or from scratch using list_nodes().

Inputs: train_graph (required). Returns: object

whoamiWhoamiAInspect

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.

Inputs: no named parameters. Returns: object

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    B3.2/5.0

    Scored across 32 tools

    Disambiguation4/5

    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.

    Naming Consistency4/5

    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.

    Tool Count2/5

    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.

    Completeness4/5

    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.

    Maintenance

    ActivityMaintained
    ResponsivenessLimited data

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      A
      quality
      C
      maintenance
      A Model Context Protocol server that provides AI agents with a unified interface for real-time EEG acquisition, replay, processing, visualization, recording, and stimulation from over 66 BrainFlow boards.
      47
      49 PyPI
      3
      BSD 3-Clause
    • A
      license
      B
      quality
      B
      maintenance
      An MCP server that gives AI assistants conversational access to MNE-Python for analyzing neurophysiology data (EEG, MEG, sEEG, ECoG, fNIRS). Enables plain-language analysis pipelines, from loading recordings to generating figures and explanations.
      41
      40 PyPI
      12
      MIT