| name | ketcher-local-harness |
| description | How to embed Ketcher in a local Flask + JavaScript page and bridge it to RDKit: read/write Molblock, convert pasted SMILES to Molblock server-side, and render validated structures into the local workspace. Use when building or extending the local structure-editor harness. |
Ketcher Local Harness
Notes for wiring Ketcher into a small local
Flask + JavaScript app and bridging it to RDKit. Ketcher is the editor, not the
whole workspace — keep it thin and let RDKit do the chemistry.
These notes are clean-room: they describe the public Ketcher API and the
server-side conversion path, and are written fresh against the Ketcher API rather
than copied from any product frontend.
Architecture
[ Ketcher iframe ] --Molblock/SMILES--> [ Flask endpoint ] --> [ RDKit ]
^ |
|------------------ validated Molblock / SVG -----------------|
- Ketcher edits structures in the browser.
- The page reads the current structure from Ketcher and sends it to Flask.
- Flask calls RDKit to validate/canonicalize/convert/render.
- Validated Molblock or rendered SVG goes back to the page and into the local
workspace.
Embedding Ketcher
Serve Ketcher's static build from web/vendor/ketcher/ and embed it in an
<iframe>. Wait for the editor to be ready before calling its API:
const frame = document.getElementById("ketcher-frame");
function ketcher() {
return frame.contentWindow.ketcher;
}
async function whenReady() {
for (let i = 0; i < 100; i++) {
if (frame.contentWindow && frame.contentWindow.ketcher) return ketcher();
await new Promise((r) => setTimeout(r, 100));
}
throw new Error("Ketcher did not become ready");
}
Reading the current structure
const k = await whenReady();
const molblock = await k.getMolfile();
const smiles = await k.getSmiles();
Loading a structure into Ketcher
Ketcher loads Molblock/Rxnfile directly:
const k = await whenReady();
await k.setMolecule(molblock);
The SMILES paste flow (server-side conversion)
Ketcher's own SMILES import can be unreliable for pasted strings. The robust path
is to convert pasted SMILES to Molblock server-side with RDKit first, then
load the Molblock:
async function loadSmiles(smiles) {
const res = await fetch("/api/convert", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ smiles, to: "molblock" }),
});
const { molblock, ok, error } = await res.json();
if (!ok) throw new Error(error);
const k = await whenReady();
await k.setMolecule(molblock);
}
Server side (Flask + RDKit):
from flask import request, jsonify
from rdkit import Chem
from rdkit.Chem import AllChem
@app.post("/api/convert")
def convert():
data = request.get_json(force=True)
smiles = (data or {}).get("smiles", "")
mol = Chem.MolFromSmiles(smiles)
if mol is None:
return jsonify(ok=False, error="invalid SMILES")
AllChem.Compute2DCoords(mol)
return jsonify(ok=True, molblock=Chem.MolToMolBlock(mol))
Rendering validated structures
Render server-side with RDKit and return SVG for the workspace preview:
from rdkit.Chem.Draw import rdMolDraw2D
def render_svg(molblock: str) -> str:
mol = Chem.MolFromMolBlock(molblock)
d = rdMolDraw2D.MolDraw2DSVG(320, 240)
rdMolDraw2D.PrepareAndDrawMolecule(d, mol)
d.FinishDrawing()
return d.GetDrawingText()
Scope constraints
- Keep Ketcher as the editor only. The object list, rendered previews, execution
trace, and workspace state live outside Ketcher (see the
workspace-json
skill).
- Do not try to recreate a full production canvas UI. This is a small local
harness: editor + tool/chat panel + object list + rendered preview + trace.
- All chemistry validation happens in RDKit on the server, never in the browser.
Maintained by the ChemIllusion team as part of OpenMolClaw.