Control BrainVoyager from AI coding agents (Claude, Zed, Cursor, etc.) via the Model Context Protocol (MCP).
┌──────────────────────┐ HTTP POST ┌──────────────────────────────┐
│ AI Agent (Claude) │ ──────────────→ │ bv_core_server.py │
│ │ │ bv_anatomy_server.py │
│ MCP Client config: │ │ bv_fmri_server.py │
│ - BV Core │ │ bv_assistant_server.py │
│ - BV Anatomy │ │ │
│ - BV fMRI │ │ Each is a FastMCP instance │
│ - BV Assistant │ │ calling _shared/bv_* │
└──────────────────────┘ └──────────┬───────────────────┘
TCP :5050 / │ WebSocket :9000
▼
┌──────────────────────────────────────────────────────────────┐
│ BrainVoyager (Qt event loop) │
│ │
│ ┌─ TCP path (document API) ─────────────────────────────┐ │
│ │ plugin/bv_plugin/mcp_listener.py ← non-blocking │ │
│ │ │ │ │
│ │ │ ALL_HANDLERS["action"](data) ← O(1) dispatch │ │
│ │ ▼ │ │
│ │ plugin/bv_plugin/listener_handlers/ │ │
│ │ ├── core_handlers.py → bv.open_document(), …│ │
│ │ ├── anatomy_handlers.py → vmr.deface(), … │ │
│ │ └── fmri_handlers.py → bv.get_vtcs_of_mdm() │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ ┌─ WS path (widget introspection) ──────────────────────┐ │
│ │ bv_inject.dylib ← DYLD_INSERT_LIBRARIES │ │
│ │ │ │ │
│ │ │ Qt accessibility tree + click / invoke │ │
│ │ ▼ │ │
│ │ MCP/_shared/bv_ws.py → query(), act() │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ plugin/bv_auto_load/mcp_helper.py │
│ → MP2RAGE denoising, utilities │
└──────────────────────────────────────────────────────────────┘
Four small servers. This follows the MCP ecosystem standard — the user enables only the servers they need per session, keeping the AI's context window lean.
| Server | Tools | What it does |
|---|---|---|
bv_core_server |
27 | Doc open/close/save/list, DICOM ops, log, shell, window |
bv_anatomy_server |
31 | VMR pipeline, MNI/Tal, mesh morphing (reconstruct, smooth, inflate, shrink-wrap), MP2RAGE |
bv_fmri_server |
22 | FMR preprocessing, VTC coregistration/creation (native/MNI/Tal), filtering, MDM |
bv_assistant_server |
4 | Experimental. Launch BV with injection, live widget tree inspection (bv_query), widget interaction (bv_act), window discovery (bv_list_windows) |
Zed agent skills give the AI
procedural knowledge about BrainVoyager workflows — from DICOM setup all
the way to VTC creation. Skills live in prompts/skills/ and are loaded
on demand based on the user's request. Copy them into your Zed skills
directory to use them.
| Skill | Covers | When to use |
|---|---|---|
| bv-expert | Complete BV knowledge base, User's Guide chapter finder, coordinate systems, GLM/normalization/coregistration concepts | Any BV conceptual question — "what is BBR?", "how does MNI work?" |
| bv-dicom-setup | DICOM rename, anonymize, series discovery, project dictionaries | "Organize my raw DICOM data" |
| bv-anatomical-pipeline | VMR creation, MP2RAGE denoising, IIHC, isovoxel, MNI/Talairach normalization, defacing | "Process my anatomical scan" |
| bv-fmri-preprocessing | FMR creation, slice timing, motion correction, HPF, spatial smoothing, EPI distortion correction (FSL topup) | "Preprocess my fMRI runs" |
| bv-coregistration-vtc | BBR and intensity-based coregistration, VTC creation in native/MNI/Tal space, VTC post-processing | "Coregister and create VTCs" |
| bv-file-formats | All BV binary formats (VMR, V16, FMR/STC, VTC, SRF, VMP, GLM, SMP, ...), axis conventions, bvbabel read/write patterns | "How do I read this VTC file?" |
Each skill's SKILL.md contains just a YAML frontmatter (name +
description) and concise instructions. When the user asks about a topic,
Zed matches the description against the request and loads the skill
automatically. The bv-expert skill also includes guide-urls.md — a
complete index of all ~190 User's Guide pages — so the agent can fetch
official documentation on demand.
# Copy all skills into your Zed project skills directory
cp -r prompts/skills/* /path/to/project/.agents/skills/Skills are auto-discovered. No restart or config change needed.
MCP/ # MCP servers + shared code (run OUTSIDE BV)
├── _shared/
│ ├── bv_client.py # call_bv() — shared by TCP-based servers
│ └── bv_ws.py # query()/act() — WebSocket widget bridge
│
└── servers/
├── bv_core_server.py
├── bv_anatomy_server.py
├── bv_fmri_server.py
└── bv_assistant_server.py # EXPERIMENTAL — widget introspection
plugin/ # Everything that runs INSIDE BrainVoyager
├── bv_ui_control/ # DYLD injection dylib (C++) + Python client
│ ├── src/ # → C++ WebSocket server injected into BV
│ ├── bv.py # → Python client for the injection bridge
│ └── CMakeLists.txt
│
├── bv_plugin/ # → Run inside BV's Python Development Panel
│ ├── mcp_listener.py # Run this from BV's Python Plugin editor
│ └── listener_handlers/ # Hash-table dispatch (no if/elif chains)
│ ├── core_handlers.py
│ ├── anatomy_handlers.py
│ └── fmri_handlers.py
│
└── bv_auto_load/ # → Copy into BV's Python Scripts directory
└── mcp_helper.py # MP2RAGE denoising, VMR utilities
Copy the files inside of plugin/bv_auto_load/ into BrainVoyager's PythonScripts folder. Then open BV's Python Development panel, open plugin/bv_plugin/mcp_listener.py, and
run it. You should see:
SUCCESS: Real-time listener active on 127.0.0.1:5050
BV's UI stays responsive thanks to a non-blocking socket polled by QTimer.
Set up python environment with uv using uv sync.
Uses uv run to pick up the project's Python environment automatically:
{
"mcpServers": {
"BrainVoyager Core": {
"command": "uv",
"args": [
"run",
"--directory",
"/path/to/bv_mcp",
"python",
"MCP/servers/bv_core_server.py"
]
},
"BrainVoyager Anatomy": {
"command": "uv",
"args": [
"run",
"--directory",
"/path/to/bv_mcp",
"python",
"MCP/servers/bv_anatomy_server.py"
]
},
"BrainVoyager fMRI": {
"command": "uv",
"args": [
"run",
"--directory",
"/path/to/bv_mcp",
"python",
"MCP/servers/bv_fmri_server.py"
]
}
}
}The assistant server lets an AI agent see and interact with BV's UI widgets
in real time (click buttons, query the widget tree, invoke Qt methods).
It uses bv_inject.dylib, which must be loaded into BV at launch:
DYLD_INSERT_LIBRARIES=/path/to/bv_mcp/plugin/bv_ui_control/build/src/libbv_inject.dylib \
/Applications/BrainVoyager.app/Contents/MacOS/BrainVoyagerAdd this server to your MCP config if you want UI automation:
"BrainVoyager Assistant": {
"command": "uv",
"args": [
"run",
"--directory",
"/path/to/bv_mcp",
"python",
"MCP/servers/bv_assistant_server.py"
]
}Selectors. Widgets are addressed with a @type:value syntax:
| Selector | Matches | Example |
|---|---|---|
@text:Open |
Any widget whose visible text contains "Open" | Click the Open button |
@name:spinBox |
Widget with Qt objectName="spinBox" |
bv_act("@name:spinBox", "stepUp") |
@class:QPushButton |
Any widget of that Qt class | Find all buttons |
Workflow: bv_query → scan the widget tree → bv_act with selector.
Replace /path/to/bv_mcp with the actual path to this project. Enable only
the servers you need — disable the rest to keep the AI's context small.
Ask your AI agent: "List the BrainVoyager methods available."
- Add a handler in
plugin/bv_plugin/listener_handlers/<domain>_handlers.py:def _vmr_new_command(data: dict) -> str: vmr = _bv.active_document result = vmr.new_command(data.get("param", "default")) return _ok(json.dumps({"result": result}))
- Register it in the
HANDLERSdict at the bottom of the same file:HANDLERS = { # ... "vmr_new_command": _vmr_new_command, # ← one line }
- Add the MCP tool in
servers/<domain>_server.py:@mcp.tool() def new_vmr_command(param: str = "default") -> str: """Docstring...""" return call_bv("vmr_new_command", timeout=30, param=param)
No changes needed in mcp_listener.py. The dispatch table picks it up automatically.