Skip to content

Instantly share code, notes, and snippets.

@ghutchis
Last active September 6, 2026 00:08
Show Gist options
  • Select an option

  • Save ghutchis/ccf0844c38aa699db7040c9f69935143 to your computer and use it in GitHub Desktop.

Select an option

Save ghutchis/ccf0844c38aa699db7040c9f69935143 to your computer and use it in GitHub Desktop.
Avogadro MCP Plan

Plan: Avogadro as a local MCP server

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:

  1. 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.
  2. Script authoring. "Screenshot every .xyz in 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, the wait rule) 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.


1. What exists today

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= and result_data(): on master (python/avogadro/connect.py).
  • avogadroapp side: commit b07bce5 on branch fix-rpc-return, not yet on master. master's rpclistener.cpp still replies the instant handleCommand() 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.
  • saveGraphic renders 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 (Command extension, InterfaceScript) are menu-only; not reachable over RPC even though they already carry a userOptions JSON schema.

2. Architecture

A Python MCP bridge, not MCP inside the C++ app

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 QTcpServer instead, 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.

Where the code lives (decided)

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.


3. Part A: the MCP server (avogadro-mcp)

3.1 Connection handling

  • 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 or avogadro-mcp --launch), ensure_running() starts Avogadro and polls internalPing for up to N seconds. Search order: AVOGADRO_APP env var, then platform defaults (/Applications/Avogadro2.app, avogadro2 on PATH, %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 RPCError codes to MCP tool errors with the plugin's own message. -3 (busy) becomes "Avogadro is still working on <cmd>; wait or retry".

3.2 Tools

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.

3.3 Resources (the script-authoring half)

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.

3.4 Testing

  • A fake RPC server (asyncio Unix socket / named pipe) that speaks the framing and answers a canned command table. It lives in avogadrolibs python/tests/ because connect.py needs it too (§5.3); avogadro-mcp imports 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.
  • mcp SDK's in-memory client for round-tripping tool/resource listings.

3.5 Client configuration and discoverability

  • avogadro-mcp --print-config [claude-desktop|claude-code|cursor] prints the JSON snippet; avogadro-mcp --install claude-desktop writes 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-mcp hint). Cheap, and it is the only route most desktop users will ever find.
  • README and a new two.avogadro.cc/source/develop/mcp.md page.

3.6 Headless rendering (in scope)

Batch jobs ("every .xyz in this directory", CI figure generation) should not need a visible window. Plan:

  1. Spike first (phase 1): launch avogadro2 with QT_QPA_PLATFORM=offscreen on macOS and Linux, connect, openFile, renderImage at 1600×1200, and compare against an on-screen render. Things that may bite: renderToImage uses an FBO so should be fine, but avogadro.cpp creates its own QOffscreenSurface at startup to probe GL, MultiViewWidget needs a non-zero size before the first render, the offscreen platform plugin has no OpenGL on some Linux builds (needs EGL or Mesa llvmpipe; document LIBGL_ALWAYS_SOFTWARE=1), and macOS may need NSApplication to exist for the app bundle to start at all.
  2. Fix whatever the spike finds in avogadroapp (probably a --headless flag that sets the platform, skips the first-launch dialogs, gives the view a default size, and disables settings writes like --disable-settings).
  3. avogadro-mcp --headless / AVOGADRO_MCP_HEADLESS=1 launches that way when auto-launch is on; the composite tools accept headless=True and 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-name argument in avogadroapp (one line in RpcListener) and a name= pass-through the Python connect already has.
  4. 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.

4. Part B: describing commands to the connection

4.1 listCommands (avogadroapp, phase 1)

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

4.2 Curated schemas in the Python package (phase 1, decided)

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.

4.3 Self-describing plugins (avogadrolibs, phase 3)

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

4.4 Python command scripts over RPC (avogadrolibs, phase 3)

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 (stringListenum, 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 _.

4.5 Display settings API (avogadrolibs + avogadroapp, phase 2, decided in principle)

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.


5. Part C: actions and commands that do not exist yet

Grouped by where they go. P1 = needed for the server to be useful at all, P2 = needed for the motivating examples, P3 = nice.

5.1 avogadroapp built-ins

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-return before anything else; every async tool depends on wait. (2 commits, rpclistener.cpp + mainwindow.cpp.)
  • exportFile should honour wait so export_file can report the real result instead of "started".
  • kill is left as is. No quit command is added.

5.2 avogadrolibs plugins (via registerCommand)

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.

5.3 The avogadro.connect client keeps pace (avogadrolibs, every phase)

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, unwrap result (or result["data"] for waited commands) and return a plain Python value. No new classes.
  • Async commands default to wait=True in the helper, because a helper that returns before the work is done is the bug the completion protocol exists to fix. send()/command() keep their wait=False default unchanged.
  • Docstrings carry the parameter table so help(connect) and the MCP scripting resource say the same thing; the rpc.md "Methods on connect" table is regenerated from those docstrings rather than edited by hand.
  • A connect.py unit test per method against the fake RPC server from §3.4, which therefore moves into avogadrolibs (python/tests/) and is reused by avogadro-mcp rather 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.

5.4 Python-only (in avogadro-mcp)

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

6. Phases

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.


7. Decisions

Taken (2026-09-05):

  1. Package location: separate avogadro-mcp repo and PyPI package.
  2. Schema source of truth: Python catalogue first, commandSchema() later.
  3. Headless rendering: in scope, best effort on Windows.
  4. No quit/kill work.
  5. Display settings: build the programmatic API (§4.5), useful beyond MCP.
  6. Command-script RPC names are slugs from package and file stem (§4.4).
  7. Scene-plugin QSettings keys are normalised to camelCase early, with a fallback read of the old keys (§4.5a).
  8. Style presets live in the avogadro-mcp package; a GUI style chooser is a separate, later TODO (§4.5c).
  9. Every new RPC method ships with a connect.py helper and an rpc.md row (§5.3); the fake RPC server lives in avogadrolibs and is shared.

Still open:

  1. 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment