Builder Tool Contract (MCP)
Status: contract version 1. Builder implements it over Streamable HTTP
(src/builder/tools/, the Tools menu on the operations rail left of the view); the
reference server is megane-builder-tools.
This page is the specification Megane Builder and third-party structure tools
implement. Change
it here first, then change the code; cite the section in the PR.
The client covers §3–§6 with these gaps, each still part of the contract:
icons are not rendered (every tool is a text entry in the Tools menu);
expectedSeconds only sets the timeout, and the progress bar shows for every
call; the element widget is a text field that takes a symbol (Fe) or an
atomic number and shows the other beside it (not the periodic-table picker), and the
cell widget takes an orthorhombic a, b, c only (pre-filled from the open
document's cell, else 20 Å) and sends it as a diagonal 3×3 cell;
a new_document tool warns in its form and, when the open document has
unsaved edits, asks before discarding them;
the §7 record is kept for the session only; the §9 confirmation is asked
only by the VS Code and JupyterLab bridges (the web app connects only to a URL
the user typed or opened), and the destination notice and chat opt-in are not
built.
Megane Builder is a JavaScript application, while most structure-generation software (packmol, ASE, pymatgen, RDKit, polymer builders, …) is Python. Instead of porting that software to TypeScript, Builder exposes tool buttons — Liquid box, Polymer chain, Solvate, … — whose work runs in an MCP server. Users add servers and get new buttons without any change to Megane.
MCP alone only says "a tool has a name and a JSON Schema". Turning a tool into
a Builder button additionally needs agreement on which tools are buttons, how
a molecule or an atom is passed in, what structure comes back, and how that
structure enters the document. This page is that agreement: a small profile on
top of MCP, identified by the _meta prefix io.github.megane-labs/.
The same server stays an ordinary MCP server, so LLM clients (Claude Code, Claude Desktop, Megane's own chat) can call the same tools. Everything below is chosen so that a tool is fully usable by a client that knows nothing of this contract.
1. Scope
In scope for contract version 1:
- Coarse, stateless generators. One call takes parameters (and, for some tools, a read-only copy of the open document) and returns atoms. Liquid and gas boxes, polymer chains, solvation, adsorbate placement, crystal builders that need a Python library.
- Two ways a result enters the document (§5): it becomes a new document, or its atoms are added to the open one.
Out of scope for version 1:
- Tools that move, delete or re-type atoms of the open document (geometry optimisation, protonation, charge assignment). They need a third apply mode that replaces the structure while keeping Builder's edit history meaningful; see §11.
- Interactive tools (anything needing more than one round trip with the user during a call). MCP elicitation is not used.
- Trajectories, volumetric data, force-field parameter files. A result is one structure.
2. Architecture
- Builder renders buttons and forms, calls tools, and applies results as ordinary Builder document changes (Undo works as usual). It stays the owner of the document; servers never hold document state.
- Bridge is host code that owns the MCP connections. A browser page cannot spawn processes, so every host needs one (§8). It is the only component that speaks MCP transports; Builder talks to it through a narrow message API.
- Servers are ordinary MCP servers. Megane ships a reference server,
megane-builder-tools, in a separate repository (§10).
3. Discovery: which tools are buttons
A tool is a Builder button if and only if its Tool definition carries
_meta["io.github.megane-labs/builder"] with a contract Builder supports.
Other tools of the same server are ignored by Builder (LLM clients still see
them).
{
"name": "liquid_box",
"title": "Liquid box",
"description": "Fill a periodic box with molecules at a target density using packmol. …",
"icons": [{ "src": "data:image/svg+xml;base64,…", "mimeType": "image/svg+xml", "sizes": ["any"] }],
"inputSchema": { "…": "§4" },
"outputSchema": { "$ref": "…BuilderResult, inlined, see §5" },
"annotations": { "readOnlyHint": true, "openWorldHint": false },
"_meta": {
"io.github.megane-labs/builder": {
"contract": 1,
"category": "bulk",
"apply": "new_document",
"document": "none",
"stochastic": true,
"expectedSeconds": 20
}
}
}
| Field | Type | Meaning |
|---|---|---|
contract | integer | Contract version. Builder hides tools whose version it does not support and says so once per server. |
category | "bulk" | "molecule" | "polymer" | "surface" | "solvation" | "other" | Tools-menu group the button is placed in. |
apply | "new_document" | "insert" | How the result enters the document (§5). Builder hides a tool with any other value and lists it separately (unknown apply mode) from tools of an unsupported contract. |
document | "none" | "optional" | "required" | Whether the tool receives the open document (§4.3). insert tools usually say required. |
stochastic | boolean | The result depends on a random seed. Such a tool must have a seed input (§4.4). |
expectedSeconds | number, optional | Typical run time; Builder shows a progress bar instead of a spinner above ~2 s and uses it to pick a timeout (§6). |
Everything a user reads comes from standard MCP fields: title is the button
label (fallback: annotations.title, then name), the first sentence of description is its tooltip,
and icons is its icon. Builder renders only data: icons; any other icon
source falls back to the category icon (this sidesteps the icon-fetching rules
in the MCP spec). description is written for both humans and LLMs: first
sentence for the tooltip, the rest for the model.
Tool names should be unique within a server; Builder always addresses a tool as
(server id from the user's configuration, tool name), so two servers may both
have liquid_box.
4. Inputs
inputSchema is a normal JSON Schema 2020-12 object schema. Builder generates
the tool's form from it. Two things are added:
- Per-property annotations
x-megane-widgetandx-megane-unit(JSON Schema ignores unknownx-keywords, and MCP already uses the same convention forx-mcp-header). They only choose the form control. The property's schema itself is always the real wire format, so a client without the contract — an LLM — can still fill it in. - A set of shared value types (§4.2) that the widgets produce.
4.1 Form generation
Builder supports this subset of JSON Schema for form generation. A tool that needs more must still validate on the server, but Builder shows its button disabled, with a "Builder cannot build a form: …" tooltip naming the property:
| Schema | Control |
|---|---|
type: "number" / "integer", with minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf | Number field (with unit suffix from x-megane-unit) |
type: "string" with enum | Select |
type: "string" | Text field (maxLength respected) |
type: "boolean" | Checkbox |
type: "array" of numbers or integers with minItems == maxItems ≤ 9 | Fixed-size vector field |
type: "array" of objects | Editable table, one row per item (Add row / Remove within minItems / maxItems); columns from the item schema; tables do not nest |
type: "object" (one level of nesting) | Fieldset |
Any property with x-megane-widget | The widget in §4.2 |
title labels the control, description becomes its help text, default
pre-fills it (else the first of examples), required marks it mandatory
(other properties without a default are labelled optional). oneOf /
anyOf / allOf / if / not / $ref to external URIs are not supported
for form generation (local $ref into $defs is). A property with a widget is exempt: its control comes from the
widget, not from its schema.
Every required property without a default should carry JSON Schema
examples. The conformance checker (§10) calls each tool with the first
example (else the default, else a generic value) of every property, and LLM
clients read them as hints. For liquid_box that is "examples": [100] on a
component's count; without it, the generic count of 1 builds a box too small
to pack and the check cannot run the tool.
4.2 Shared value types and widgets
The JSON Schemas below are normative. A server's property with a given
x-megane-widget must declare a schema that accepts exactly this shape
(it may be stricter, e.g. minItems). The reference SDK (§10) exports them.
x-megane-widget: "molecule" — a molecule from the Builder library.
The control is a drop-down of the Builder library (the presets and the user's
sketches, imported files and saved selections). Builder
sends the library molecule as a V2000 mol block with explicit hydrogens and 3D
coordinates in Å — what the library already stores after RDKit embedding.
{
"type": "object",
"properties": {
"name": { "type": "string" },
"molblock": { "type": "string", "description": "V2000 mol block, explicit H, 3D, Å" },
"smiles": { "type": "string" }
},
"required": ["name"],
"anyOf": [{ "required": ["molblock"] }, { "required": ["smiles"] }]
}
Builder always sends name and molblock (library entries carry no SMILES). Servers must accept molblock and may accept smiles alone, which
is what LLM clients will usually send; a server that cannot embed a SMILES
returns a tool error saying so. A server must not re-embed or re-optimise a
3D molblock it was given: the user chose that conformer.
Whether a mol block is 3D is stated by the dimension code in its header
(columns 21–22 of the second line, 3D or 2D), never guessed from the
coordinates: a flat molecule is a valid 3D geometry (Builder's benzene preset
has z = 0 for every atom). Builder writes 3D for library molecules and 2D
for the ones marked planar (imported 2D files). Servers that need 3D input
return a tool error for a 2D mol block rather than guessing a conformer, and
treat a missing code as 3D.
x-megane-widget: "atom" — one atom of a molecule argument. Used, for
example, for the head and tail atoms of a polymer monomer. The value is a
0-based index into the atom block of that molecule's molblock. The property
names the molecule argument it refers to with x-megane-of; the control lists
that molecule's atoms (index and element) to pick from, and is a plain number
field until a molecule is chosen.
{ "type": "integer", "minimum": 0, "x-megane-widget": "atom", "x-megane-of": "monomer" }
x-megane-widget: "element" — an atomic number, {"type": "integer", "minimum": 1, "maximum": 118}, shown as the periodic-table picker.
x-megane-widget: "cell" — a row-major 3×3 cell in Å, {"type": "array", "items": {"type": "number"}, "minItems": 9, "maxItems": 9}. Builder pre-fills
it with the open document's cell when there is one, and offers the
orthorhombic a, b, c shortcut.
x-megane-widget: "selection" — atom indices into the document argument
(§4.3), {"type": "array", "items": {"type": "integer", "minimum": 0}}.
Builder fills it with the atoms selected in the 3D view; the form only shows
how many are selected.
x-megane-widget: "seed" — {"type": "integer", "minimum": 0}. Builder
pre-fills a fresh random value, shows it (editable, with a New seed button),
and records it (§7).
x-megane-widget: "document" — see §4.3. Never shown as a control; the
form says how many atoms the open structure has.
4.3 The open document
A tool whose _meta says document: "required" or "optional" declares
exactly one property with x-megane-widget: "document" and the
Structure schema. Builder fills it with the structure
currently shown in the 3D view — the document with its edits applied, not the
file as opened (unless the History panel's Show original is on, which also
blocks insert tools) — as elements, positions, cell, bonds and, when
any bond is not single, bondOrders. selection indices refer to atoms of
this payload.
The document is read-only input. An insert tool returns only the atoms it
adds, never the document's atoms (§5.2); this is what keeps the Builder's edit
history, atom references and Undo valid across a tool call.
4.4 Units and conventions
Values are always in the canonical unit; x-megane-unit is only the label and
the conversion the control offers. The canonical units are:
x-megane-unit | Quantity | Canonical unit |
|---|---|---|
angstrom | length | Å |
degree | angle | ° |
g/cm3 | density | g cm⁻³ |
kelvin | temperature | K |
count | number of molecules / repeat units | 1 |
Anything else (e.g. mol/L) is displayed verbatim and not converted.
Every stochastic tool declares a seed property. Given the same arguments,
the same seed and the same server and library versions, a tool should
return the same structure.
5. Results
A successful call returns structuredContent conforming to BuilderResult,
declares that schema as its outputSchema, and, per the MCP spec, also returns
a short text summary in content (for LLM clients, e.g. "Built a 3.1 nm box
with 1000 water and 20 ethanol molecules (3160 atoms).").
{
"contract": 1,
"name": "water-ethanol",
"structure": { "…": "Structure, §5.1" },
"warnings": ["packmol reached the iteration limit; minimum distance 1.7 Å < tolerance 2.0 Å"],
"provenance": { "packmol": "21.2.3", "rdkit": "2026.03.6", "megane-builder-tools": "0.1.0", "seed": 12345 }
}
| Field | Meaning |
|---|---|
contract | Must equal the tool's contract. |
name | Suggested document / fragment name. Builder sanitises it and uses it as the file name for new_document and as the stem of the fragment id for insert; a missing or blank name becomes result. |
structure | The atoms (§5.1). |
warnings | Non-fatal problems, shown in Builder's notice line after the text summary. A result that is unusable is an error (§6), not a warning. |
provenance | Versions of the software that produced the result and the seed actually used. Free-form object of strings / numbers; recorded by Builder (§7). |
5.1 Structure
The payload is a structure-of-arrays JSON object that maps directly onto
Megane's Snapshot and on to the fields the writers export. It is used both
for results and for the document input.
| Field | Type | Required | Notes |
|---|---|---|---|
elements | integer[] | yes | Atomic numbers 1–118, length N. 0 is not allowed. |
positions | number[] | yes | Cartesian Å, flat [x0,y0,z0, x1,…], length 3N. |
cell | number[9] | null | yes | Row-major lattice vectors in Å, or null for a non-periodic structure. |
bonds | [int, int][] | yes | 0-based pairs of two distinct atoms. Complete: every bond the tool knows. |
bondOrders | integer[] | no | Parallel to bonds: 1, 2, 3, or 4 (aromatic). Omitted means all single. |
molecules | integer[] | no | Per-atom 0-based molecule index. |
residueNames | string[] | no | Per-atom residue name (≤ 4 chars recommended for PDB export). |
residueIds | integer[] | no | Per-atom residue number. |
atomNames | string[] | no | Per-atom name. |
chainIds | string[] | no | Per-atom single-character chain id. |
formalCharges | integer[] | no | Per-atom formal charge. |
scalars | object of number[] | no | Named per-atom channels Megane does not render yet (e.g. "partial_charge"). Kept, not dropped. |
Rules:
- All per-atom arrays have length N; mismatched lengths make the result invalid.
- Builder draws exactly the bonds the payload lists and infers none. A
tool that builds molecules must return their bonds; a tool that genuinely
has no connectivity (e.g. an atomic gas) returns
[]. This is the same "the source asserts, nothing is invented" principle Megane's parsers follow (rule #11 inAGENTS.md). - Molecules should be returned whole (not wrapped into the cell); Builder's Structure › Wrap atoms into cell does wrapping when the user wants it.
- Size: Builder accepts up to 500 000 atoms per result. Tools that can exceed this should expose a limit parameter. A binary encoding for large payloads is reserved for a later contract version.
5.2 Apply modes
apply | What Builder does | Undo |
|---|---|---|
new_document | Warns in the form (The result replaces the open structure and its history.) when the open document has edits and, on Run, asks Discard unsaved edits? before the call; then opens the result as a new document (openStructure), named from name. Residue names become the document's labels (RESNAME<resid> when residueIds is given). | The previous document and its history are replaced, like File › Open… or New …. |
insert | Needs an open, editable document (Run is disabled otherwise). Converts the result into one add_fragment op (absolute positions, no translation, bonds and orders from the payload) with a fresh fragment id, and pushes it. If the result has a cell that differs from the document's, a set_cell op (without atom scaling) is pushed first, in the same step. The new atoms are selected. | One Undo removes the whole insertion. |
Because insert goes through the existing add_fragment op, a tool-inserted
molecule is indistinguishable from a library placement: later edits address it
as <fragmentId>:<k>, and the document replays without the server.
6. Execution, errors, cancellation
- Builder calls a tool only on an explicit user action (button → form → Run). It never calls tools on its own.
- Builder validates the form against the constraints of
inputSchemait renders (§4.1; Run stays disabled until they hold) before calling, and validatesstructuredContentagainst its own copy ofBuilderResult(not the server-providedoutputSchema) before applying anything. - Calls carry a
progressToken. Servers should sendnotifications/progressfor anything longer than a couple of seconds; themessageis shown under the progress bar. - Cancel sends
notifications/cancelled. Servers should stop the external process (packmol, …) promptly. Builder discards any result that arrives after cancellation. - Timeout:
max(60 s, 5 × expectedSeconds), and at most 30 minutes. Hitting it behaves like Cancel and reports a timeout. - A stdio server writes nothing but MCP messages to stdout. Libraries that print (RadonPy does) must be silenced or redirected to stderr, or the bridge loses the connection.
- Failures a user can fix (invalid combination of parameters, packmol did not
converge, a planar molecule where 3D is required) are tool errors:
isError: truewith a one-line, actionable message in the first text content item. Builder shows that line in the form and keeps the form open with the user's values. JSON-RPC and transport errors are shown the same way, as "The server failed: …" with the error's message. - A result that fails validation (§5.1) is rejected entirely; Builder never applies a partial structure.
7. Provenance and reproducibility
For every applied result Builder records:
{
"server": { "id": "megane-builder-tools", "name": "megane-builder-tools", "version": "0.1.0" },
"tool": "liquid_box",
"contract": 1,
"arguments": { "…": "as sent, with the document argument replaced by its atom count and a SHA-256 of its payload" },
"provenance": { "…": "from the result" }
}
Builder keeps these entries in the document's history (next to the edit ops) and writes them into saved Builder documents, so a structure can be rebuilt by calling the same tool with the same arguments. The persistence format is part of the Builder document work and is not fixed by this contract.
Today Builder keeps the record in memory for the session only (the tools
store's applied list), not in the edit history or a saved file. Each entry has
server: { name, version, url } (from the server's serverInfo and the URL
it connected to), tool, contract, arguments with the document argument
replaced by { "atoms": N } (no hash yet), the result's provenance, and the
number of atoms applied.
8. Hosts and the bridge
Servers are configured in the common mcpServers format, so the same entry
works in LLM clients:
{
"mcpServers": {
"megane-builder-tools": {
"command": "uvx",
"args": [
"--from", "git+https://github.com/hodakamori/megane-builder-tools",
"megane-builder-tools"
]
},
"lab-tools": { "url": "https://tools.example.org/mcp" }
}
}
Builder runs in the standalone webapp (/builder.html) and, embedded, in the
VS Code extension, the JupyterLab extension and the Jupyter widget
(megane.Builder). The webapp, VS Code and JupyterLab show the Tools menu;
the Jupyter widget turns it off (BuilderHost.tools: false,
src/builder/host.ts; see docs/docs/platform-support.md). The webapp uses the direct path: the page
connects to a Streamable HTTP server itself (the
Tools › Tool server… form takes the server URL, default
http://127.0.0.1:8765/mcp, and bearer token), whether the page came
from megane serve, a local build or the hosted docs. The server has to allow
the page's origin (megane-builder-tools --transport http --allow-origin <origin>). stdio servers need a bridge that spawns them; the rows below say
where each bridge goes, and they are part of this contract so that the
page-side API does not change when one is added.
VS Code has the bridge (vscode-megane/src/builderTools.ts): the extension
host reads the megane.builder.mcpServers setting (stdio entries with
command / args / env / cwd, Streamable HTTP entries with url /
headers; ${workspaceFolder} and ${userHome} are replaced), asks once per
command line or URL before it first starts or reaches a server, and connects
with the MCP TypeScript SDK. The Builder page gets a ToolBridge
(BuilderHost.toolBridge, src/builder/tools/bridge.ts) whose connections
behave like direct ones, so the Tools menu, forms and apply path are shared.
Builder tabs share one connection per server; a stdio server is stopped when
the last tab disconnects or closes. The setting is restricted in untrusted
workspaces, and an HTTP entry must use HTTPS unless it is on loopback.
Both bridges offer the reference server without any configuration
(megane-builder-tools, §10): the megane-builder-tools command if it is on
PATH (or in uv's ~/.local/bin, which an editor started from the Dock does not
see), else uvx --from git+https://github.com/hodakamori/megane-builder-tools@<commit> megane-builder-tools at a commit pinned in Megane (DEFAULT_SERVER_SOURCE in
builderTools.ts and config.py, kept equal by a test). An entry named
megane-builder-tools replaces it and null hides it; the usual confirmation
still comes first. A stdio server inherits the host's whole environment, not
the SDK's short list, so uvx's first download sees the proxy and uv settings;
the VS Code client allows 10 minutes for that first initialize.
JupyterLab has the bridge in the Jupyter server (megane.builder_tools,
a server extension enabled by etc/jupyter/jupyter_server_config.d/megane.json
in the wheel). It reads the same entries from MeganeBuilderTools.mcp_servers
in the server's configuration (${workspaceFolder} is the server's root
directory) and connects with the MCP Python SDK, the megane[tools] extra.
The labextension's Builder talks to it over a WebSocket
(/megane/builder-tools/ws, authenticated like the server's other sockets,
and open only to users who may execute kernels) with the same messages as
VS Code's postMessage. Before the first connection to a server, the page
asks with the command line or URL and remembers the answer in the browser,
keyed by the server's fingerprint (command, args, env and cwd, or URL). Tabs
and browser windows share one connection per server.
A page opened with #tools=<url>&token=<token> in its address connects on
load and removes the fragment from the address bar; a tool server can print
such a link. The fragment is never sent to a web server, so the token does not
end up in access logs.
A build can name a default server with VITE_BUILDER_TOOLS_URL; Builder
connects to it on load while it is the chosen server (no token). Megane's demo
site uses this for a hosted reference server running in public mode
(megane-builder-tools --public): no token, since one embedded in a public page
would be public too, and only requests whose Origin is the demo site are
answered. That keeps other sites and crawlers out but is not authentication; the
server bounds each call's time and the number of concurrent calls.
| Host | Bridge | Config location | stdio | HTTP |
|---|---|---|---|---|
Any webapp page (hosted, local build, megane serve) — implemented | None: the page connects to HTTP servers directly | Tools › Tool server…; the URL is remembered in local storage, the token is not | no | CORS permitting |
Standalone webapp via megane serve (planned) | FastAPI app spawns servers and relays over its WebSocket | megane serve --mcp-config <file> | yes | yes |
| VS Code extension — implemented | Extension host spawns servers and relays through postMessage (src/builder/tools/bridgeProtocol.ts) | megane.builder.mcpServers setting | yes | yes (HTTPS, or HTTP on loopback) |
| JupyterLab labextension — implemented | Jupyter server extension (megane.builder_tools) spawns servers and relays over a WebSocket | MeganeBuilderTools.mcp_servers in jupyter_server_config | yes | yes (HTTPS, or HTTP on loopback) |
| Jupyter widget | — | — | — | — (megane.Builder has no Tools menu; MolecularViewer does not mount Builder) |
The bridge (src/builder/tools/bridgeProtocol.ts) answers only these page
messages: tools:servers (the configured servers), tools:connect (the
server's name, version and raw tools/list), tools:call (with
tools:progress notifications), tools:cancel, tools:disconnect and
tools:openSettings. It never
forwards arbitrary JSON-RPC, so a page cannot reach resources, prompts or
sampling. Hosts that get a bridge must be listed in
docs/docs/platform-support.md when this ships (rule #6).
9. Trust and security
- Adding a server is installing software. A stdio entry runs an arbitrary command as the user. Builder / the host asks for confirmation when a server is added, showing the command line or URL, and remembers the decision per server.
- Remote servers receive your structures. For an HTTP server, the tool form
states the destination host whenever the tool takes the
documentor amoleculeargument. HTTP servers the bridge talks to must use HTTPS unless they are on loopback. - Loopback servers are an attack surface. A server listening on localhost
for the static webapp must check
Originand require a token, as the MCP Streamable HTTP transport requires; the reference server does both. A public demo server without a token must still refuse requests whoseOriginis not its page, and must cap call time and concurrency. - Everything a server sends is untrusted. Titles, descriptions, names,
warnings and error messages are rendered as plain text. Icons are rendered
only from
data:URIs of an allowed image type. Result sizes are capped (§5.1) before parsing. - LLM exposure is opt-in per server. Megane's chat can offer a server's tools to the model only when the user enables that for the server, because third-party tool descriptions are prompt input.
10. Repositories and conformance
- This contract lives in Megane (this page), because Megane is the client that has to implement it and it is versioned with Builder.
- The reference server lives in its own repository,
hodakamori/megane-builder-tools, installed from Git (uvx --from git+https://github.com/hodakamori/megane-builder-tools megane-builder-tools; it is not on PyPI yet). It is split out for the same reasons asmegane-rdkit: its dependencies (packmol, RadonPy, RDKit, their licences) and release cadence have nothing to do with Megane's wheel and CI. It contains:megane_builder_tools.sdk— theStructure/BuilderResult/Moleculemodels, the widget and unit annotations of this page (MoleculeInput,DocumentInput,OptionalDocumentInput,atom_of(...),Seed,Cell,Selection,Length,Density, …), a@builder_tool(...)decorator on top of the official MCP Python SDK (MCPServer) that fills_meta,outputSchema, the text summary andprovenance, runs synchronous tools in a worker thread and passes aProgressreporter, and converters from/to ASEAtomsand RDKitMol;- the reference tools (§12) and the
megane-builder-toolsserver (stdio, or Streamable HTTP on loopback withOriginchecks and a bearer token); megane-builder-conformance, a checker that connects to any server (a stdio command or an HTTP URL) and verifies §3–§6: markers and contract version, the form-generation subset, widget schemas,seedon stochastic tools, document rules,outputSchema; then calls every tool with sample arguments (§4.1) and validates the result (lengths, complete bonds, bond orders, aninsertresult that does not repeat the document), that the same seed reproduces the structure, and that a call without the required arguments is refused. Cancellation is not checked yet. Third-party servers are expected to pass it.
- Megane's own tests use recorded
tools/list/tools/callfixtures from the reference server (make fixturesthere, committed undertests/fixtures/builder-tools/and read bytests/ts/builder/tools/), so Megane CI never needs Python tool dependencies.
11. Versioning
contract is a single integer. Additive, optional changes (a new widget, a new
optional Structure field, a new category) keep the number; Builder ignores
what it does not know, falling back to the plain control for an unknown
widget and to other for an unknown category. Anything that changes the
meaning of an existing field, adds an apply mode, or makes a field required
bumps it. Builder supports a list of contract versions and hides tools outside
it with a notice.
Planned for later versions: a replace apply mode (and a matching
whole-structure edit op) for tools that modify the open document; a binary
position encoding for very large results.
12. Worked examples
These are the three tools megane-builder-tools ships. Together they cover
both apply modes and the molecule, atom, seed and document widgets.
12.1 Liquid box — new_document
{
"name": "liquid_box",
"title": "Liquid box",
"description": "Fill a periodic box with molecules at a target density using packmol. Give each component and its number of copies; the box is sized so that the mixture has the requested density (g/cm³). …",
"inputSchema": {
"type": "object",
"properties": {
"components": {
"type": "array",
"title": "Components",
"minItems": 1,
"maxItems": 8,
"items": {
"type": "object",
"properties": {
"molecule": { "$ref": "#/$defs/Molecule", "x-megane-widget": "molecule", "title": "Molecule" },
"count": { "type": "integer", "minimum": 1, "maximum": 500000, "title": "Count", "examples": [100], "x-megane-unit": "count" }
},
"required": ["molecule", "count"]
}
},
"density": { "type": "number", "exclusiveMinimum": 0, "maximum": 5, "default": 1.0, "title": "Density", "x-megane-unit": "g/cm3" },
"shape": { "type": "string", "enum": ["cubic", "orthorhombic"], "default": "cubic", "title": "Box shape" },
"aspect": { "type": "array", "items": { "type": "number", "exclusiveMinimum": 0 }, "minItems": 3, "maxItems": 3, "default": [1, 1, 1], "title": "Aspect ratio a:b:c" },
"tolerance": { "type": "number", "minimum": 1.0, "maximum": 4.0, "default": 2.0, "title": "Minimum distance", "x-megane-unit": "angstrom" },
"seed": { "type": "integer", "minimum": 0, "x-megane-widget": "seed", "title": "Seed" }
},
"required": ["components", "seed"],
"$defs": { "Molecule": { "…": "§4.2 molecule schema" } }
},
"_meta": {
"io.github.megane-labs/builder": {
"contract": 1, "category": "bulk", "apply": "new_document",
"document": "none", "stochastic": true, "expectedSeconds": 20
}
}
}
Result: cell is the box, molecules / residueNames identify each copy
(residue name from the molecule name, truncated to 4 characters), all
intramolecular bonds and orders are copied from the input mol blocks. packmol not reaching the
tolerance is a warning; packmol failing outright is a tool error.
12.2 Polymer chain — new_document
Built with RadonPy's random-walk
polymerisation (radonpy.core.poly.polymerize_rw, MMFF94 optimisation after
every step). RadonPy marks the junctions of a monomer with two linker atoms;
the tool turns the hydrogen the head atom gives up and the one the tail atom
gives up into those linkers, and the two linkers left at the chain ends back
into hydrogens. Tacticity is neither controlled nor checked: every unit keeps
the monomer's stereochemistry.
{
"name": "polymer_chain",
"title": "Polymer chain",
"description": "Build a linear homopolymer with RadonPy's random-walk polymerisation. The head and tail atoms are the ones that bond to the neighbouring units; each gives up one hydrogen at every junction, so the chain ends keep theirs. …",
"inputSchema": {
"type": "object",
"properties": {
"monomer": { "$ref": "#/$defs/Molecule", "x-megane-widget": "molecule", "title": "Monomer", "examples": [{ "name": "ethylene", "smiles": "CC" }] },
"head": { "type": "integer", "minimum": 0, "x-megane-widget": "atom", "x-megane-of": "monomer", "title": "Head atom", "examples": [0] },
"tail": { "type": "integer", "minimum": 0, "x-megane-widget": "atom", "x-megane-of": "monomer", "title": "Tail atom", "examples": [1] },
"seed": { "type": "integer", "minimum": 0, "x-megane-widget": "seed", "title": "Seed" },
"length": { "type": "integer", "minimum": 1, "maximum": 200, "default": 10, "title": "Repeat units", "x-megane-unit": "count" }
},
"required": ["monomer", "head", "tail", "seed"]
},
"_meta": {
"io.github.megane-labs/builder": {
"contract": 1, "category": "polymer", "apply": "new_document",
"document": "none", "stochastic": true, "expectedSeconds": 60
}
}
}
Result: no cell (null); residueIds numbers the repeat units, so the viewer
can colour or filter by unit. A head or tail atom without a hydrogen to give
up, head equal to tail, or RadonPy giving up on the random walk are tool
errors. Because RadonPy re-optimises the whole chain at every step, run time
grows steeply: about 40 s for 100 ethylene units and 7 minutes for 200, hence
the 200-unit maximum.
12.3 Solvate — insert
{
"name": "solvate",
"title": "Solvate",
"description": "Fill the empty space of the open structure's cell with solvent molecules. The number of solvent molecules is chosen so that the solvent has the requested density (g/cm³) in the part of the cell not taken by the existing atoms' van der Waals spheres. … Only the added solvent is returned; the open structure is not changed. Needs an orthorhombic cell.",
"inputSchema": {
"type": "object",
"properties": {
"document": { "$ref": "#/$defs/Structure", "x-megane-widget": "document" },
"solvent": { "$ref": "#/$defs/Molecule", "x-megane-widget": "molecule", "title": "Solvent" },
"seed": { "type": "integer", "minimum": 0, "x-megane-widget": "seed", "title": "Seed" },
"density": { "type": "number", "exclusiveMinimum": 0, "maximum": 5, "default": 1.0, "title": "Density", "x-megane-unit": "g/cm3" },
"tolerance": { "type": "number", "minimum": 1.0, "maximum": 4.0, "default": 2.0, "title": "Minimum distance", "x-megane-unit": "angstrom" }
},
"required": ["document", "solvent", "seed"]
},
"_meta": {
"io.github.megane-labs/builder": {
"contract": 1, "category": "solvation", "apply": "insert",
"document": "required", "stochastic": true, "expectedSeconds": 30
}
}
}
Result: only the solvent atoms, with cell equal to the document's cell (so
Builder pushes no set_cell). packmol keeps the document's atoms fixed and
every solvent atom at least tolerance from other atoms across periodic
boundaries. A document without a cell, or with a non-orthorhombic cell, is a
tool error telling the user to set a cell (in Builder: Structure › Set cell…).
12.4 Server side
Writing a Builder Tool Server walks through a complete server, from installing the SDK to buttons in the Builder. With the reference SDK a tool is a typed Python function:
from typing import Annotated, Literal
from pydantic import Field
from megane_builder_tools.sdk import BuilderResult, Density, Length, Progress, Seed, builder_tool
@builder_tool(title="Liquid box", category="bulk", apply="new_document", expected_seconds=20)
async def liquid_box(
components: Annotated[list[Component], Field(min_length=1, title="Components")],
seed: Seed,
density: Annotated[Density, Field(gt=0, title="Density")] = 1.0,
shape: Annotated[Literal["cubic", "orthorhombic"], Field(title="Box shape")] = "cubic",
tolerance: Annotated[Length, Field(ge=1.0, title="Minimum distance")] = 2.0,
progress: Progress = Progress(),
) -> BuilderResult:
"""Fill a periodic box with molecules at a target density using packmol. ..."""
...
The decorator derives inputSchema (with the widget annotations), sets
outputSchema, _meta and stochastic (from the Seed parameter) and
document (from a DocumentInput parameter), writes the text summary, fills
provenance with the seed and the package version, hides the Progress
parameter from the schema and connects it to MCP progress notifications, and
turns ToolError exceptions into isError results.