Goal: let an MCP client on the same machine (Claude Desktop, Claude Code,
Cursor, anything speaking MCP) drive a running Avogadro, discover what it can
do, and get enough context to write correct standalone Python scripts against
the avogadro PyPI package.
Two audiences, one mechanism:
- Interactive driving. "Open this file, show the HOMO, give me a picture." The MCP client calls tools; each tool is a JSON-RPC message to Avogadro.
- Script authoring. "Screenshot every
.xyzin this directory." Claude Code does not need to drive Avogadro one call at a time; it needs to know the vocabulary (avogadro.connect, the command list, the option names, thewaitrule) so it writes a script that works first time. The same discovery data that feeds the tools feeds this, exposed as MCP resources.
Decisions taken 2026-09-05 (Geoff): separate avogadro-mcp repo and
package; command schemas start as a Python catalogue; auto-launch off by
default pending community feedback; headless rendering is in scope even if
Windows cannot do it; no quit over RPC; a programmatic display-settings API
is wanted in its own right, not only for MCP; every new RPC method also gets
a helper on avogadro.connect so plain scripts benefit, not only MCP users.
Wire protocol (avogadroapp/avogadro/rpc/, rpclistener.cpp): JSON-RPC 2.0
over a local socket named avogadro ($TMPDIR/avogadro on Unix,
\\.\pipe\avogadro on Windows), 4-byte big-endian length prefix. One request,
one reply. Documented at two.avogadro.cc/source/develop/rpc.md.
Built-in methods (RpcListener::messageReceived + MainWindow::handleCommand):
openFile, loadMolecule, exportFile, saveGraphic, setProjection,
setRenderTypes, internalPing, kill (unreachable: --testing is rejected by
the argument parser; left as is, see decisions).
Plugin commands: 28, registered through ExtensionPlugin::registerCommand /
ToolPlugin::registerCommand(QString name, QString description) and dispatched
through handleCommand(QString, QVariantMap). Plugins: Bonding, Crystal,
SpaceGroup, ResetView, Navigator, Surfaces, Vibrations, AlignTool. MainWindow
already keeps m_commandDescriptionsMap but nothing reads it back.
Completion protocol: wait / timeout reserved params, commandStarted /
commandFinished(message, result) / commandFailed signals, error codes
-2 (failed/timed out) and -3 (plugin busy).
- avogadrolibs side: on
master(26b18486). - Python client with
wait=andresult_data(): onmaster(python/avogadro/connect.py). - avogadroapp side: commit
b07bce5on branchfix-rpc-return, not yet onmaster.master'srpclistener.cppstill replies the instanthandleCommand()returns.
Python client: avogadro.connect.connect in the PyPI wheel. Pure Python,
~300 lines, handles framing, Windows pipes, timeouts, RPCError.
Scene plugin settings: every scene plugin keeps its own values (atom
scale, bond radius, opacity, label options, colours) in per-layer interface
structs, seeded from QSettings keys like ballandstick/atomScale and
label/color, and changed only through the widget returned by
ScenePlugin::setupWidget(). There is no programmatic getter or setter, and no
description of which settings exist. App-level render settings (background
colour, AO/DOF/fog on the solid pipeline, projection) are likewise only
reachable through dialogs, apart from setProjection.
Gaps that matter for MCP:
- No way to read anything back: no command list, no molecule contents, no atom count, no camera state, no "is a basis set loaded". A client is blind.
saveGraphicrenders at the on-screen widget size only and writes to disk.- Command descriptions are one translated sentence; no parameter names, types, or defaults anywhere machine-readable. The docs page is the only schema.
- Display appearance cannot be set without the mouse.
- Python command scripts (
Commandextension,InterfaceScript) are menu-only; not reachable over RPC even though they already carry auserOptionsJSON schema.
MCP client (Claude Desktop / Claude Code)
│ MCP over stdio (JSON-RPC 2.0, MCP framing)
▼
avogadro-mcp (Python, FastMCP from the official `mcp` SDK)
│ avogadro.connect — length-prefixed JSON-RPC over local socket
▼
Avogadro (RpcListener → MainWindow → plugins)
Why a bridge:
- MCP's standard local transport is stdio: the client spawns the server
process. A GUI app cannot be that process. Avogadro would have to serve
Streamable HTTP over
QTcpServerinstead, and then every client needs a URL and a running app before it can connect at all. - The MCP spec moves fast (transports, auth, elicitation, structured output).
A pure-Python server updates with
pip; the C++ app ships a few times a year. - The bridge is where "help me write a script" lives anyway: it can hand out docs, examples, and the command catalogue without Avogadro running.
- Avogadro's job stays small and stable: answer JSON-RPC, describe itself.
Cost: the user needs Python and pip install avogadro-mcp. Mitigated by
letting Avogadro itself install the MCP client config (section 3.5) and, later,
by running the server from Avogadro's bundled pixi environment.
New repo OpenChemistry/avogadro-mcp, pure Python, published as
avogadro-mcp on PyPI, console script avogadro-mcp. Depends on avogadro
(for connect and cjson) and mcp. It releases on its own cadence; the
compiled avogadro wheel does not gate server fixes. Vendor a copy of
connect.py as a fallback import so the server still starts when the compiled
wheel is missing or broken for a platform.
- Lazily connect on first tool call; reconnect on
ConnectionError. - Auto-launch is off by default (pending community feedback). When Avogadro
is not listening, tools fail with a clear "Avogadro is not running; start it
or set
AVOGADRO_MCP_LAUNCH=1" message. With the opt-in (env var oravogadro-mcp --launch),ensure_running()starts Avogadro and pollsinternalPingfor up to N seconds. Search order:AVOGADRO_APPenv var, then platform defaults (/Applications/Avogadro2.app,avogadro2onPATH,%ProgramFiles%\Avogadro2\bin\avogadro2.exe). - One Avogadro instance at a time (the socket name is fixed). Document it.
- Serialize calls: FastMCP tools are
async, the socket client is blocking; run calls in a single worker thread so two tools never interleave on the socket. - Map
RPCErrorcodes to MCP tool errors with the plugin's own message.-3(busy) becomes "Avogadro is still working on<cmd>; wait or retry".
Two layers. Curated tools have hand-written docstrings and argument types so a model uses them well. Everything else is reachable through a generic tool.
Curated (phase 1, mostly maps onto existing RPC):
| Tool | RPC | Notes |
|---|---|---|
avogadro_status |
internalPing, version, moleculeInfo |
First thing a model calls. Running? Version? What is loaded? |
open_file(path) |
openFile |
Resolve to absolute path first. |
load_molecule(content, format) |
loadMolecule |
|
get_molecule(format="cjson") |
getMolecule (new) |
Returns text. |
molecule_info() |
moleculeInfo (new) |
Atom count, formula, unit cell, basis set, orbitals, cubes, vibrations, conformers, selection size. |
export_file(path) |
exportFile with wait |
|
screenshot(width, height, transparent, path=None) |
renderImage (new) |
Returns MCP ImageContent (base64 PNG) when no path. |
list_display_types() |
listDisplayTypes (new) |
Identifiers, enabled flags, and each type's settings schema (§4.5). |
set_display(types) |
setRenderTypes |
|
get_display_settings(type) / set_display_settings(type, settings) |
new (§4.5) | Atom scale, bond radius, opacity, label options, colours. |
set_render_settings(...) |
new (§4.5) | Background colour, AO/DOF/fog, projection. |
apply_display_style(name_or_json) |
setDisplayStyle (new, §4.5) |
Whole-scene presets such as "publication" or "cpk". |
rotate(x, y, z), zoom(delta), translate(x, y), reset_view() |
rotateScene, zoomScene, translateScene, alignView |
|
get_camera() / set_camera(...) |
new | Reproducible figures; needed for movies. |
render_surface(kind, **opts) |
renderVanDerWaals/renderMO/… with wait=True |
One tool, kind enum. Returns the result map. |
select_atoms(...) / clear_selection() |
new | |
run_command(name, params, wait, timeout) |
anything | The escape hatch. Docstring points at list_commands. |
list_commands() |
listCommands (new) |
Returns the catalogue (§4). |
No quit tool. Closing Avogadro stays a user action; the existing kill
method is left untouched (and still unreachable).
Composite (phase 2, Python-side, no new C++):
| Tool | Built from |
|---|---|
rotation_movie(axis, degrees_per_frame, frames, width, height, out) |
loop rotateScene + renderImage; encode with imageio-ffmpeg (optional extra) or write a frame directory plus the ffmpeg command |
screenshot_directory(glob, out_dir, style, size) |
openFile + alignView + setDisplayStyle + renderImage per file; runs headless when asked (§3.6) |
orbital_gallery(orbitals, isovalue) |
renderMO with wait + renderImage per orbital |
Composites are also the worked examples for the script-writing resources:
each one has a standalone-script twin in examples/.
Dynamic (phase 3): when listCommands returns a JSON schema for a
command, register it as an MCP tool at startup (FastMCP.add_tool with the
schema as inputSchema). Third-party plugins then appear automatically.
Curated tools shadow dynamic ones with the same RPC name.
| URI | Content |
|---|---|
avogadro://commands |
Live listCommands output as JSON; falls back to the bundled catalogue when Avogadro is not running |
avogadro://molecule |
Current molecule as CJSON |
avogadro://molecule/info |
moleculeInfo |
avogadro://display |
Enabled display types, their settings, render settings: the current "style" as JSON, ready to be saved as a preset |
avogadro://docs/scripting |
Markdown: how to use avogadro.connect, the wait rule, error codes, the -3 busy rule, Windows pipe caveat, headless launch. Sourced from rpc.md so it never diverges from the website |
avogadro://docs/commands |
The command tables from rpc.md |
avogadro://examples/<name> |
Each composite tool's script twin |
Plus one prompt: write_avogadro_script(task) that pulls in the scripting
doc, the live command list, and molecule_info, then asks for a script using
avogadro.connect. This is what makes "generate a screenshot for every .xyz"
come out right in Claude Code.
- A fake RPC server (
asyncioUnix socket / named pipe) that speaks the framing and answers a canned command table. It lives in avogadrolibspython/tests/becauseconnect.pyneeds it too (§5.3);avogadro-mcpimports it as a test dependency. Every tool tested against it, no GUI needed, runs in CI on all three OSes. - One optional end-to-end test behind an env flag that runs against a real Avogadro started headless (§3.6) on Linux and macOS CI.
mcpSDK's in-memory client for round-tripping tool/resource listings.
avogadro-mcp --print-config [claude-desktop|claude-code|cursor]prints the JSON snippet;avogadro-mcp --install claude-desktopwrites it.- In Avogadro: Settings → Scripting → Set up MCP… button that runs the
same installer through the bundled Python (or shows the snippet if the
package is missing, with a
pip install avogadro-mcphint). Cheap, and it is the only route most desktop users will ever find. - README and a new
two.avogadro.cc/source/develop/mcp.mdpage.
Batch jobs ("every .xyz in this directory", CI figure generation) should
not need a visible window. Plan:
- Spike first (phase 1): launch
avogadro2withQT_QPA_PLATFORM=offscreenon macOS and Linux, connect,openFile,renderImageat 1600×1200, and compare against an on-screen render. Things that may bite:renderToImageuses an FBO so should be fine, butavogadro.cppcreates its ownQOffscreenSurfaceat startup to probe GL,MultiViewWidgetneeds a non-zero size before the first render, the offscreen platform plugin has no OpenGL on some Linux builds (needs EGL or Mesa llvmpipe; documentLIBGL_ALWAYS_SOFTWARE=1), and macOS may needNSApplicationto exist for the app bundle to start at all. - Fix whatever the spike finds in
avogadroapp(probably a--headlessflag that sets the platform, skips the first-launch dialogs, gives the view a default size, and disables settings writes like--disable-settings). avogadro-mcp --headless/AVOGADRO_MCP_HEADLESS=1launches that way when auto-launch is on; the composite tools acceptheadless=Trueand start a private instance under a different socket name (avogadro-headless-<pid>) so it never collides with the user's open session. This needs a--rpc-nameargument inavogadroapp(one line inRpcListener) and aname=pass-through the Pythonconnectalready has.- Windows: attempt it, do not promise it. The offscreen plugin there has no GL; the fallback is a minimized real window, which the spike should also try.
New built-in method. Returns:
{ "result": [
{ "name": "renderMO",
"description": "Render a molecular orbital.",
"kind": "extension", "plugin": "Surfaces",
"async": true,
"schema": { "type": "object", "properties": { ... }, "required": [] } },
{ "name": "openFile", "kind": "builtin", ... }
]}name, description, kind, plugin come from data MainWindow already
holds (m_commandDescriptionsMap, m_toolCommandMap, m_extensionCommandMap),
plus a hand-written table for the builtins. schema and async are empty in
phase 1 and filled in by 4.2/4.3.
Also version: app version, avogadrolibs version, Qt version, platform,
rpcProtocol: 2 (1 = no wait; lets the Python side warn when talking to an
old build).
avogadro_mcp/catalogue.json: one JSON-schema entry per command in rpc.md
today (28 plugin + builtins). The server overlays this on whatever
listCommands returns, so the MCP tools have real parameter types on day one
without touching any plugin. Marked "source": "catalogue" so the eventual
app-provided schema wins when present.
Keep rpc.md and the catalogue from drifting: a doc-build check that every
command in the catalogue appears in rpc.md and vice versa.
Non-breaking addition to ExtensionPlugin and ToolPlugin:
/**
* Describe the options a registered command accepts, as a JSON Schema
* object (type, properties, required, enum, default, description).
* Return an empty object for commands with no options or no description.
* Set "x-avogadro-async": true if the command emits commandStarted().
*/
virtual QJsonObject commandSchema(const QString& command) const;Default implementation returns {}. No signal signature changes, so
third-party plugins compile unchanged. MainWindow::listCommands asks each
owning plugin lazily. Schemas are plain JSON Schema so the MCP server passes
them straight through as inputSchema.
Then fill it in for the eight plugins that register commands today, moving the
prose in rpc.md into the code (the docs page can eventually be generated
from listCommands).
The Command extension already loads each script's userOptions. Have it
registerCommand() one RPC name per script and implement handleCommand by
runCommand(options, molecule) with commandStarted/commandFinished around
the background run. commandSchema translates userOptions types
(stringList → enum, integer, float, boolean, string, filePath)
to JSON Schema. Every installed avogadro-* plugin then becomes an MCP tool
with zero extra work by the plugin author.
Naming (decided): a slug, not the translatable display name. Plugin
packages are already named after the tool they wrap (avogadro-xtb,
avogadro-stk) and appear under that name in Manage Plugins, so the slug is
what users recognise. Derive it as <package>.<script-stem> (e.g.
avogadro-xtb.optimize), falling back to the script file stem alone for
scripts outside a package. The MCP tool name is the same slug with - and
. replaced by _.
Wanted on its own merits: scripted figures, saved styles, and eventually a "Display styles" chooser in the Display Types dock. Three layers.
a. ScenePlugin gains a settings interface (non-breaking, defaults empty):
/**
* JSON Schema describing the settings this plugin exposes, e.g.
* {"type":"object","properties":{"atomScale":{"type":"number",
* "minimum":0,"maximum":1,"default":0.3,"description":"..."}}}.
* Colours are strings "#rrggbb" or "#rrggbbaa"; enums list their choices.
*/
virtual QJsonObject settingsSchema() const;
/** Current values for the active layer, keyed as in settingsSchema(). */
virtual QVariantMap settings() const;
/**
* Apply one or more settings to the active layer. Unknown keys and values
* of the wrong type are reported in @p errors and skipped; the rest are
* applied. Emits drawablesChanged() if anything changed. Persists to
* QSettings exactly as the setup widget would.
*/
virtual bool setSettings(const QVariantMap& values, QStringList* errors = nullptr);The setup widget becomes a view over the same values: its slots call
setSettings(), and a settingsChanged() signal lets the widget refresh when
a script changes something. Each plugin's per-layer interface struct already
holds the values, so the implementation is mostly moving the existing
opacityChanged-style slots behind one map. Key names are camelCase and match
the schema exactly (atomScale, bondRadius, multiBonds, showHydrogens,
opacity; atomOptions, bondOptions, residueOptions, radiusScalar,
labelScale, color for labels).
Normalise the QSettings keys first, as its own small PR (decided; do it
soon to limit disruption). Today they are mixed case (label/radiusscalar,
label/atomoptions next to ballandstick/atomScale). Rename every scene
plugin key to <plugin>/<camelCaseKey>, and on first read fall back to the
old spelling so existing users keep their settings. Keep the fallback for a
release or two, then drop it. This lands before the settings interface so the
interface never has to know about old spellings.
Roll-out order: BallStick, Licorice, VanDerWaals, Wireframe, Label, SurfaceRender, Cartoons, then the rest. Plugins not yet converted simply report an empty schema.
b. App-level render settings (avogadroapp): getRenderSettings /
setRenderSettings covering backgroundColor, projection, ambientOcclusion,
depthOfField, fog (and their strengths), multisample. These are the
values in MainWindow::setBackgroundColor and the SolidPipeline setters
today. setProjection stays as an alias.
c. Styles (avogadroapp): a style is one JSON document:
{ "displayTypes": {"BallStick": {"enabled": true, "settings": {"atomScale": 0.25}},
"Label": {"enabled": false}},
"render": {"backgroundColor": "#ffffff", "ambientOcclusion": true},
"camera": { "projection": "perspective" } }getDisplayStyle returns the current one; setDisplayStyle(style) applies
a style document. The app only understands documents; named presets
(publication, cpk, wireframe, cartoon) live as JSON in the
avogadro-mcp package for now (decided), and apply_display_style(name) on
the MCP side resolves the name before calling the RPC. A style chooser or
"Save current display as style…" in the GUI is a separate TODO, out of scope
here; when it happens the same documents move into app resources.
RPC surface: listDisplayTypes (now includes each type's settingsSchema),
getDisplaySettings(type), setDisplaySettings(type, settings),
getRenderSettings, setRenderSettings(...), getDisplayStyle,
setDisplayStyle(...). The MCP tools in §3.2 map onto these one-to-one.
Grouped by where they go. P1 = needed for the server to be useful at all, P2 = needed for the motivating examples, P3 = nice.
| Command | Params | Pri | Notes |
|---|---|---|---|
listCommands |
none | P1 | §4.1 |
version |
none | P1 | §4.1 |
moleculeInfo |
none | P1 | atomCount, bondCount, formula, charge, multiplicity, hasUnitCell, hasBasisSet, orbitalCount, homoIndex, cubeCount, vibrationCount, coordinateSetCount, selectedAtomCount, hasResidues, fileName |
getMolecule |
format (default cjson) |
P1 | FileFormatManager::writeString. Large molecules: fine, the socket has no size limit beyond memory |
getAtoms |
selectedOnly |
P2 | {elements: [...], positions: [[x,y,z],...]}; cheaper than CJSON for a model to reason over |
renderImage |
width, height, transparentBackground, fileName (optional) |
P1 | renderToImage(QSize) already takes a size. Without fileName, return {"png": "<base64>"}. Replaces the "screen size only" limit of saveGraphic |
listDisplayTypes |
none | P1 | identifiers + display names + enabled flag + settings schema; today the client has to guess |
getDisplaySettings / setDisplaySettings |
type, settings |
P2 | §4.5a |
getRenderSettings / setRenderSettings |
see §4.5b | P2 | includes background colour |
getDisplayStyle / setDisplayStyle |
style JSON | P2 | §4.5c; preset names are resolved on the Python side |
getCamera / setCamera |
modelView (16 floats), projection |
P2 | There is an old save-and-load-camera stash in avogadroapp doing exactly this |
newMolecule |
none | P2 | Reset to empty |
setActiveTool |
name |
P2 | MainWindow::setActiveTool(QString) exists |
resizeView |
width, height |
P3 | renderImage covers most of it |
listTools |
none | P3 | |
undo / redo |
none | P3 | RWMolecule undo stack |
--rpc-name <name> (CLI flag) |
P2 | Lets a headless instance listen on its own socket (§3.6) | |
--headless (CLI flag) |
P2 | Whatever the §3.6 spike shows is needed |
Also:
- Merge
fix-rpc-returnbefore anything else; every async tool depends onwait. (2 commits,rpclistener.cpp+mainwindow.cpp.) exportFileshould honourwaitsoexport_filecan report the real result instead of "started".killis left as is. Noquitcommand is added.
| Command | Plugin | Params | Pri | Notes |
|---|---|---|---|---|
selectAtoms |
Select | indices or elements or residues |
P2 | Logic already in the Select extension's menu actions |
selectAll, clearSelection, invertSelection |
Select | none | P2 | |
getSelection |
Select | none | P2 | Or fold into moleculeInfo/getAtoms |
addHydrogens / removeHydrogens |
Hydrogens | none | P2 | |
optimizeGeometry |
ForceField | method, maxSteps, tolerance |
P2 | Async with commandStarted; returns final energy. FIRE/L-BFGS already in calc/ |
calculateEnergy |
ForceField | method |
P2 | Returns energy + units |
setCoordinateSet |
(app or Animation) | index |
P2 | Conformers/trajectories; frames for movies of a trajectory |
centerMolecule |
ResetView | none | P3 | alignView mostly covers it |
renderMovie |
new or Animation | frames, axis, degrees, fileName |
P3 | Native version of the Python composite; only worth it if ffmpeg-free output (image sequence) is wanted in the app |
runScript |
Command | script name + options | P2 | §4.4 |
Scene-plugin appearance (labels, colours, radii) is covered by §4.5 rather than by per-plugin commands.
Every new built-in or plugin command lands with a matching method on
avogadro.connect.connect, so plain scripts get the same vocabulary as MCP
users. avogadro-mcp is a consumer of connect, not the owner of it. The
rule: an RPC method is not done until connect.py has a method for it and
rpc.md has a row for it. The app change is an avogadroapp PR and the
client change is an avogadrolibs PR, so link them and merge the app side
first; the client method against an older app just raises RPCError
METHOD_NOT_FOUND, which is the right failure.
Phase 1 additions to connect:
| Method | RPC | Returns |
|---|---|---|
version() |
version |
dict |
list_commands() |
listCommands |
list of dicts (name, description, kind, plugin, schema) |
molecule_info() |
moleculeInfo |
dict |
get_molecule(format="cjson") |
getMolecule |
str |
render_image(width, height, transparent=False, filename=None) |
renderImage |
bytes of PNG when no filename, else True. No PIL dependency; callers decode if they want to |
list_display_types() |
listDisplayTypes |
list of dicts |
set_display_types(types) |
setRenderTypes |
wrapper for an existing method that had no helper |
set_projection(kind) |
setProjection |
same |
export_file(filename, wait=True) |
exportFile |
gains wait now that the app honours it |
Phase 2 additions follow the same pattern: get_camera() / set_camera(),
get_display_settings(type) / set_display_settings(type, **values),
get_render_settings() / set_render_settings(**values),
get_display_style() / set_display_style(style), new_molecule(),
set_active_tool(name), select_atoms(...) / clear_selection(),
optimize_geometry(...), set_coordinate_set(index).
Conventions for these methods:
- Thin: one
send()call, unwrapresult(orresult["data"]for waited commands) and return a plain Python value. No new classes. - Async commands default to
wait=Truein the helper, because a helper that returns before the work is done is the bug the completion protocol exists to fix.send()/command()keep theirwait=Falsedefault unchanged. - Docstrings carry the parameter table so
help(connect)and the MCP scripting resource say the same thing; therpc.md"Methods on connect" table is regenerated from those docstrings rather than edited by hand. - A
connect.pyunit test per method against the fake RPC server from §3.4, which therefore moves into avogadrolibs (python/tests/) and is reused byavogadro-mcprather than duplicated.
avogadro-mcp calls these helpers when the installed avogadro wheel has
them and falls back to send() with the same payload when it does not, so the
server works against a wheel that predates a given helper.
rotation_movie,screenshot_directory,orbital_gallery(§3.2).- Headless launcher and private-instance handling (§3.6).
- Style presets shipped as JSON in the package until the app bundles its own.
| Phase | Repo | Work | Size |
|---|---|---|---|
| 0 | avogadroapp | Merge fix-rpc-return; exportFile honours wait; --rpc-name flag |
small |
| 1 | avogadroapp | listCommands, version, moleculeInfo, getMolecule, renderImage, listDisplayTypes; headless spike and whatever --headless needs |
~500 lines, two PRs |
| 1 | avogadrolibs (python) | connect.py helpers for every phase 1 built-in (§5.3), fake RPC server + tests under python/tests/, regenerate the rpc.md methods table |
one PR, paired with the app PR |
| 1 | avogadro-mcp | Package skeleton, connection layer, curated tools for everything in §3.2 phase 1 that exists, catalogue.json, resources, tests against the shared fake server, --print-config |
new repo |
| 1 | avogadrolibs | Normalise scene-plugin QSettings keys to camelCase with old-key fallback (§4.5a) |
one small PR, early |
| 2 | avogadrolibs | ScenePlugin settings interface (§4.5a) for the seven common scene plugins |
one PR for the interface, one per plugin or small group |
| 2 | avogadroapp | Render settings and style documents (§4.5b, c); getCamera/setCamera, newMolecule, setActiveTool |
2–3 PRs |
| 2 | avogadrolibs | Select and Hydrogens commands, setCoordinateSet, ForceField commands |
2 PRs |
| 2 | avogadrolibs (python) | connect.py helpers for the phase 2 commands (§5.3) |
paired with each app/plugin PR |
| 2 | avogadro-mcp | Display/style tools with bundled preset JSON, composite tools + example scripts, write_avogadro_script prompt, opt-in auto-launch and headless launch |
|
| 3 | avogadrolibs | commandSchema() virtual, schemas for the 8 existing plugins, Command-script exposure (§4.4) |
one PR per plugin group |
| 3 | avogadro-mcp | Dynamic tool registration from schemas | |
| 4 | avogadroapp, website | Settings → "Set up MCP" button; mcp.md; generate the rpc.md command tables from listCommands |
Phases 1 (app) and 1 (Python) can proceed in parallel; the Python side runs
against the fake server until the app side lands. The §4.5 interface PR is
the one to open early for review, since it touches the public ScenePlugin
API that external plugins subclass.
Taken (2026-09-05):
- Package location: separate
avogadro-mcprepo and PyPI package. - Schema source of truth: Python catalogue first,
commandSchema()later. - Headless rendering: in scope, best effort on Windows.
- No
quit/killwork. - Display settings: build the programmatic API (§4.5), useful beyond MCP.
- Command-script RPC names are slugs from package and file stem (§4.4).
- Scene-plugin
QSettingskeys are normalised to camelCase early, with a fallback read of the old keys (§4.5a). - Style presets live in the
avogadro-mcppackage; a GUI style chooser is a separate, later TODO (§4.5c). - Every new RPC method ships with a
connect.pyhelper and anrpc.mdrow (§5.3); the fake RPC server lives in avogadrolibs and is shared.
Still open:
- Auto-launch: off by default for now; revisit after community feedback. The opt-in env var / flag ships regardless so the behaviour can flip without a code change.