Overview & conventions
Tool names are stable namespace.verb. Each maps to exactly one domain; that domain token is both a klink.find_tools domain=<token> navigation key and a --profile <token> filter.
name*in a parameter list means required.- Coordinates:
_um= microns (most natural);_dbu= integer database units.micron = dbu × layout.dbu, wheredbucomes fromlayout.info. - Layers: layer index
int,"L/D"string (e.g."1/0"), or{layer, datatype}object. - Badges: read write verify escape mutates (undoable) long (separate timeout).
shape.insert_boxes / shape.insert_many / instance.insert_many / instance.insert_pcell_many. Singleton inserts are for debugging one object.Discover tools live
The catalogue is queried live, never memorized:
from klink import KLinkClient
with KLinkClient() as c:
print([m["name"] for m in c.methods()["methods"]]) # meta.methods
klink.find_tools # no args → domain index
klink.find_tools domain="routing_backends" # that area's tools + usage
klink.find_tools query="lvs route" # ranked matches across all tools
klink.guide reports what is open, the on-disk intent state (declared nets / LVS reports / spec files), and the literal next call for each available intention.
Profiles
--profile filters along two orthogonal axes — intent and domain. Default is read,write,verify,escape.
| Intent | Exposes |
|---|---|
| read | Read-only: layout.info, cell.list, shape.query, view.*, pcell.*, recorder. |
| write | Editing: shape.insert_*, cell.create, layer.ensure, instance.insert*, edit.undo. |
| verify | Checks: drc.run, lvs.run. |
| escape | Escape hatches: exec.python, exec.reset, events.*. |
all | Everything, no filtering. |
python -m klink.mcp --profile read,write,verify,escape # default
python -m klink.mcp --profile read,device_photonics # mix both axes
python -m klink.mcp --profile routing_backends # one domain only
All local tools are always included under any intent profile; klink.find_tools / klink.status / klink.reconnect are always on. Legacy aliases: basic→read, draw→write, advanced→escape, drc→verify.
1 · Connection, self-check, discovery & view connection_and_view · 15 tools
Start here when unsure. klink.status reports connection, active session, interpreter and optional capabilities; klink.guide reports what is open + on-disk intent state + the literal next call; klink.find_tools navigates the rest; klink.reconnect recovers a dropped link. A freshly created cell is invisible until view.show_cell. Screenshots (view.screenshot) are a user-requested artifact only — never an agent verification step; prefer geometry queries.
| Tool | Params | Function |
|---|---|---|
klink.status | — | MCP bridge connection status, active session, interpreter, last connection error. First stop when debugging. |
klink.reconnect | — | Close a stale client and try to reconnect to KLayout. |
klink.guide | — | Reports what is open, on-disk intent state, and the literal call for each available intention + a suggested next action. |
klink.find_tools | domain, query | Discover tools by domain or keyword. |
hello | client, protocol | Introduce the client, receive server info + capabilities. Recommended first call per connection. |
meta.ping | — | Liveness probe; echoes params + trace id. |
meta.methods | — | Full RPC method catalogue with descriptions + JSON schemas, ready for LLM function-calling. |
meta.debug_signals | fire | SignalHub diagnostics; optionally fire a synthetic event to test delivery. |
view.list_tabs | — | List layout tabs (index, title, file, active cell, current). |
view.activate_tab mutates | index* | Switch the current tab; all single-layout RPCs then act on it. |
view.show_cell | cell*, zoom_fit | Set the displayed top cell (makes a new cell visible), zoom-fits by default. |
view.screenshot | mode, width_px, height_px, bbox_dbu, path | Render a PNG. base64 data URL or path to disk. User-requested only. |
view.zoom_fit mutates | — | Fit the whole layout into the viewport. |
view.zoom_box mutates | bbox_dbu* | Zoom to exactly the given bbox (dbu). |
view.viewport | — | Report current viewport: visible bbox (dbu), pixel size, cellview index. |
view.show_lvsdb | path*, kind | Load a saved LVS/netlist DB into the Netlist Browser and show it. lvs→.lvsdb, l2n→.l2n. |
klink.status
klink.guide
view.list_tabs
from klink import KLinkClient
with KLinkClient() as c:
print(c.hello(client="my-script"))
print(c.layout_info(verbosity="summary"))
2 · Multi-session registry & cross-session transfer multi_session_transfer · 15 tools
klink drives many KLayout sessions from one MCP bridge (each a port in the local registry, typically 8765, 8766, …). Sessions are equal peers — pass the one you mean explicitly. Transfer is two-phase and confirmation-safe: transfer_prepare builds a package, dry-runs it on the target, and persists it pending; transfer_commit writes it.
| Tool | Params | Function |
|---|---|---|
klink.session_list | include_stale | Enumerate discoverable sessions. |
klink.session_status | session_id, include_stale | One session record (default: active). |
klink.session_label | session_id*, label*, aliases, description | Attach a human label and aliases. |
klink.session_resolve | query* | Resolve id / label / alias / active cell / top cell to a session. |
klink.session_use | session_id* | Repoint the bridge's primary RPC target. |
klink.session_set_klive_target | session_id* | Choose the klive-compatible 8082 endpoint (where c.show() lands). |
klink.transfer_prepare | source_session*, target_session*, target_cell, copy_mode, layer_map, translate_um | Build a flat-selection package and dry-run it on the target. |
klink.transfer_commit | package_id*, dry_run | Commit a prepared package. |
session.mark_klive_target | — | (plugin) Mark this window as the klive/8082 target. |
session.label_set | session_id*, label*, aliases, description | (plugin) Set label/aliases in the shared registry. |
transfer.pending_set | package* | Store a reviewed package in this window. |
transfer.pending_status | — | Pending package status for this window. |
transfer.pending_clear | — | Clear the pending package (no geometry written). |
transfer.paste_pending mutates | dry_run, clear_after | Paste the pending package (already contains final layers/coords). |
transfer.import_cell_tree_package mutates long | path*, source_cell*, dry_run | Import one cell tree from a GDS/OAS via native Cell.copy_tree; conflicts get $N. |
klink.session_list
klink.session_label session_id="klayout-8766" label="scratch" aliases=["test"]
klink.transfer_prepare source_session="klayout-8765" target_session="klayout-8766" copy_mode="flat_selection"
klink.transfer_commit package_id="pkg_0001"
3 · Geometry & cell-structure authoring geometry_authoring · 28 tools
The core drawing surface. Read first: layout.info, cell.list/cell.tree, layer.list, shape.query, instance.query, pcell.*. Author with batch RPCs. layer.ensure before drawing. Edits are transactional; edit.undo/redo/status. Destructive layout.clear/cell.delete only on disposable test cells.
Read
| Tool | Params | Function |
|---|---|---|
layout.info | verbosity | Snapshot: views, active cellview, top cell, source file, dbu, top-cell list, layers. Refresh your world view. |
cell.list | name_prefix, top_only, with_bbox, offset, limit | Flat paginated cell list. |
cell.tree | root, max_depth, max_nodes | Hierarchical cell tree (bounded). |
layer.list | — | All layers: layer_index, layer/datatype, name, dbu_um. |
shape.query long | cell*, layers, bbox_dbu, kinds, limit | Read shapes from ONE cell (no recursion) as JSON. Narrow with layers+bbox; paginate (default 500, max 5000). |
instance.query | parent*, child, bbox, limit | Direct child instances: name, bbox, transform, array, PCell metadata, per-layer shape counts. |
pcell.libraries | — | Available PCell libraries (Basic + PDKs). |
pcell.list | library | PCell names in a library (default Basic). |
pcell.info | library, pcell* | Parameters of a PCell (name/type/default/description/choices). |
Write · cells, layers, shapes, instances
| Tool | Params | Function |
|---|---|---|
cell.create mutates | name | New cell (dup name → $1…). Not idempotent. |
cell.rename mutates | cell*, new_name*, allow_suffix | Rename; dup name errors unless allow_suffix=true. |
cell.delete mutates | cell*, recursive | Delete a cell (recursive drops orphaned children). Destructive. |
layer.ensure mutates | layer*, datatype, name | Upsert a GDS layer; returns layer_index. |
shape.insert_boxes mutates | cell, layer, boxes_um/boxes_dbu | Batch many rectangles on one cell/layer. |
shape.insert_many mutates | cell*, items*, dry_run | Batch mixed box/polygon/path/text. |
shape.insert_box mutates | cell, layer, bbox | One rectangle. |
shape.insert_polygon mutates | cell, layer, points | One polygon (hull only), auto-closed. |
shape.insert_path mutates | cell, layer, points, width, begin_ext, end_ext, round_ends | One path (center line + width). |
shape.insert_text mutates | cell, layer, position, size_um | One text label (annotation, no mask geometry). |
shape.delete mutates | cell, layer(s), bbox, kinds, dry_run | Delete shapes by selector; dry_run previews count. One transaction. |
instance.insert_many mutates | parent*, items*, dry_run | Batch child-cell instances. |
instance.insert_pcell_many mutates | parent*, items*, dry_run | Batch PCell instances. |
instance.insert mutates | parent*, child*, position, rotation, mirror, magnification, array | Place child in parent; optional array grid. |
instance.insert_pcell mutates | parent*, library, pcell*, params, position, … | Build a PCell variant then insert. Call pcell.info first. |
instance.delete mutates | parent*, child, bbox, all, limit, dry_run | Delete instances by selector (non-destructive to the child cell). |
pcell.register_fitted | name*, fit_table* | Register a fitted-device PCell at runtime into klink_structdevice; no plugin reload. |
Write · layout-level & edit history
| Tool | Params | Function |
|---|---|---|
layout.show_file mutates long | path*, mode, keep_position, technology | Load GDS/OAS (reload if open). replace or new tab. |
layout.save_file mutates | path*, cellview_index | Save; extension picks GDSII/OASIS. |
layout.clear mutates | cellview_index | Destructive: clear the whole layout. |
edit.undo mutates | — | Undo last undoable op; returns before/after stack. |
edit.redo mutates | — | Redo. |
edit.status | debug | Undo/redo availability. |
layout.info verbosity="summary"
cell.create name="MYBLOCK"
layer.ensure layer=1 datatype=0 name="M1"
shape.insert_boxes cell="MYBLOCK" layer="1/0" boxes_um=[[0,0,10,4],[20,0,30,4],[40,0,50,4]]
shape.insert_many cell="MYBLOCK" items=[
{"kind":"path","layer":"2/0","points_um":[[0,20],[50,20]],"width_um":2},
{"kind":"text","layer":"63/0","position_um":[0,26],"text":"MYBLOCK","size_um":4}
]
view.show_cell cell="MYBLOCK"
pcell.info library="Basic" pcell="CIRCLE"
instance.insert_pcell parent="MYBLOCK" library="Basic" pcell="CIRCLE" params={"l":"1/0","r":5.0,"n":64} position_um=[100,0]
4 · Selection & SEND interaction memory selection_and_send_memory · 11 tools
Two distinct things. selection.* is the live current selection; interaction.* is the durable, session-scoped memory of selections the user explicitly SENT (recorded as ids like sel_0006). Resolve by order/count, not age. Bind user phrases ("just sent", "this area", "that one") to these ids, not to screenshots.
| Tool | Params | Function |
|---|---|---|
selection.get | limit | Current selection (shape or instance). Empty = empty list, not an error. |
selection.set_box mutates | cell*, bbox_dbu*, layers, limit | Select all shapes intersecting a box on given layers; replaces current selection. |
selection.clear mutates | — | Clear the current selection. |
selection.send_context | source, max_items | Emit the current selection as a selection_sent event (agent-side SEND). |
interaction.selection.latest | — | Latest stored SEND. |
interaction.selection.recent | limit | Recent SENDs (default latest 5, by order). |
interaction.selection.get | id* | One stored selection by id. |
interaction.selection.label | id*, label, description | Attach a label/description. |
interaction.selection.clear_session | confirm* | Clear this session's interaction context after confirmation. |
interaction.context | include_current_selection | Current selection + recent SEND memory together. |
# user selects A → SEND, selects B → SEND
interaction.selection.recent limit=2 # → [sel_0007(B), sel_0006(A)]
interaction.selection.label id="sel_0006" label="probe pad"
5 · Ports & anchors (routing markers) ports_and_anchors · 16 tools
Ports are net endpoints (klink_Port PCells: net + orientation + width). Anchors are routing constraints (klink_Anchor PCells) whose kind is waypoint_region, bend_region, or corridor. Routing tools default port_layer=999/99, anchor_layer=999/1. Keepouts are NOT an anchor kind — pass your own design's obstacle_layers to routing tools. These are the input to routing: mark Ports+Anchors, then call a routing.* tool.
| Tool | Params | Function |
|---|---|---|
port.set_layer mutates | layer* | Configure the default Port marker layer. |
port.mark mutates | cell*, name, center, orientation, width_um, port_type, net, target_layer, … | Create one klink_Port PCell (a net endpoint). |
port.list | cell*, layer, sort | List Ports in a cell. |
port.update mutates | cell*, name*, orientation, width_um, net, … | Update one Port by immutable name. |
port.transform mutates | cell*, names / selection, rotate_delta, net, … | Batch-update Ports by names or GUI selection. |
port.repair_names mutates | cell*, layer, prefix | Repair duplicate/empty Port names (GUI-inserted). |
port.unmark mutates | cell*, name* | Delete one Port. |
port.delete_all mutates | cell*, layer | Delete all Ports in a cell. |
anchor.set_layer mutates | layer* | Configure the default Anchor marker layer. |
anchor.mark mutates | cell*, kind, center, net, radius_um, width_um, height_um, path_points, … | Create one klink_Anchor PCell (routing constraint). |
anchor.list | cell*, layer, sort | List Anchors in a cell. |
anchor.update mutates | cell*, id*, new_id, kind, net, … | Update one Anchor by immutable id. |
anchor.transform mutates | cell*, ids / names / selection, kind, … | Batch-update Anchors. |
anchor.repair_ids mutates | cell*, layer, prefix | Repair duplicate/empty Anchor ids. |
anchor.unmark mutates | cell*, id* | Delete one Anchor. |
anchor.delete_all mutates | cell*, layer | Delete all Anchors in a cell. |
port.mark cell="NET1" name="A" center_um=[0,0] orientation="E" width_um=2 net="sig"
port.mark cell="NET1" name="B" center_um=[80,20] orientation="W" width_um=2 net="sig"
anchor.mark cell="NET1" kind="waypoint_region" center_um=[40,40] radius_um=6 net="sig"
routing.tapered_hybrid_cell cell="NET1" angle_mode="manhattan" obstacle_layers=["10/0"]
6 · Routing backends routing_backends · 9 tools
All read Port/Anchor PCells and write routes. Pick by topology/quality. Always inspect the structured result: ok=false, obstacle_hit_count>0, sibling overlaps, short route_count all mean failure. Pass your own design's obstacle_layers (no default).
| Tool | Params | Function |
|---|---|---|
routing.tapered_hybrid_cell | cell*, spacing_um, angle_mode, clear, port_layer, anchor_layer, obstacle_layers | Main path+patch backend. angle_mode: any/manhattan/fortyfive. |
routing.tapered_polygon_cell | cell*, …, route_layer, corner_style, obstacle_layers | Continuous taper polygons (first-class). |
routing.steiner_cell | cell*, route_layer, root_ports, clear, obstacle_layers | Multi-terminal nets (>2 ports). |
routing.damped_segment_cell | cell, damping_distance_um, obstacle_layers, … | Extra obstacle clearance, segment output. |
routing.damped_polygon_cell | cell, damping_distance_um, corner_style, … | Damped continuous taper polygons. |
routing.damped_steiner_cell | cell, damping_distance_um, root_ports, … | Damped multi-terminal trunk/branch. |
routing.global_channel_cell | cell*, spacing_um, angle_mode, safe_distance_um, obstacle_layers | Stronger global-decision router: candidate assignment + corridor-capacity load-balancing, reuses hybrid geometry. |
routing.multilayer_escape_cell | cell*, route_layer*, bridge_layer*, via_layer*, spacing_um, obstacle_layers | Wall-blocked nets via bridge layer + vias. |
routing.gdsfactory_ports | cell*, route_layer*, router, cross_section, separation_um, radius_um, waypoints_um, path_length_match, obstacle_bboxes_um, … | Route Port markers with one named gdsfactory strategy. Needs gdsfactory in the interpreter. |
routing.gdsfactory_ports routers: bundle (default, Manhattan river routing), electrical (sharp corners + electrical typing), sbend (offset facing ports), all_angle (non-Manhattan + optional backbone_um spine), single (per-pair Manhattan), dubins (arc-based any-heading), astar (experimental grid A* — klink verifies and errors on wall-crossing). A parameter the chosen router can't honor is an error, never silently ignored.
routing.tapered_hybrid_cell cell="BLOCK" angle_mode="manhattan" spacing_um=20 obstacle_layers=["900/0"]
routing.steiner_cell cell="BLOCK" route_layer="1/0"
routing.gdsfactory_ports cell="BLOCK" route_layer="1/0" router="bundle" separation_um=5 radius_um=10 path_length_match=true
match=True — marker counts and "looks routed" never substitute.7 · DRC & LVS verification drc_and_lvs_verification · 2 tools
Both are long-running, pure-pya, domain-agnostic. drc.run runs arbitrary DRC DSL you supply — exceptions come back as results, they do NOT fail the RPC. lvs.run extracts the live layout into a device netlist and compares it against a reference netlist you supply, writes a .lvsdb, and (default) opens the Netlist/LVS browser. For structdevice flows prefer structdevice.lvs_check.
| Tool | Params | Function |
|---|---|---|
drc.run mutates long | (DRC DSL source + optional output_rdb) | Run arbitrary DRC DSL in KLayout's Ruby DRC engine. With source() = standalone, else interactive on the current layout. |
lvs.run long | cell*, conductors*, vias, devices*, reference*, out_lvsdb, show | Extract (per-cell extractors + conductor layers) → compare to reference (reference.spice or reference.netlist) → write .lvsdb and show. |
drc.run script="m1 = input(1,0)\nm1.space(0.2.um).output('M1_space','M1 spacing < 0.2um')"
lvs.run cell="BLOCK" conductors=["1/0","3/0"] vias=["2/0"] devices={...} reference={"spice":"ref.spice"} out_lvsdb="block.lvsdb" show=true
8 · Custom-device netlist → auto P&R → LVS device_structdevice · 7 tools
Device-agnostic custom-device P&R. A "device" is any cell with an arbitrary parameter set + terminals; klink assumes no device vocabulary. The device library, process profile, and terminal source are example/PDK data passed in explicitly — the tools ship none and return an instructive "write/run an example" error. Compact physical model: the device's own metal layers double as routing layers; mode 2L/3L picks 2 or 3.
| Tool | Params | Function |
|---|---|---|
structdevice.build_from_netlist | cell*, netlist*, confirm, mode, rows, cols, session | Headline one-call flow: device-level netlist → floorplan → single-pass multilayer routes → draw → device-LVS a fresh cell. Confirmation-gated (call once → proposal; again with confirm → build). |
structdevice.declare_nets | recent_sends*, cell*, conductors, vias | One SEND framing ≥2 terminals = one declared net (persisted). Example-driven. |
structdevice.connect_nets | cell*, session, route_layer, route_width_um, via_cell, min_spacing_um, min_width_um, conductors, vias | Wire declared-but-unconnected nets + verify; any LVS mismatch undoes everything. |
structdevice.lvs_check | cell*, session, mode, conductors, vias | Net-level reconcile; device/both also run device-level NetlistComparer. |
structdevice.spec_write | cell*, layer_roles*, session, device_class, conductors, vias | Project a live cell into a klink.spec.json fact file. |
structdevice.register_pcell | name*, fit_table*, session | Register a fitted-device PCell at runtime; zero plugin reloads. |
pcell.register_fitted | name*, fit_table* | The plugin RPC behind register_pcell. |
# step 1: no confirm → proposal
structdevice.build_from_netlist cell="RINGOSC" netlist={"instances":[...], "nets":[...], "groups":[]} mode="3L"
# step 2: same args + confirm token → build
structdevice.build_from_netlist cell="RINGOSC" netlist={...} mode="3L" confirm="CONFIRM-xyz"
# SEND-driven interactive path
structdevice.declare_nets recent_sends=3 cell="BLOCK" conductors=["1/0","3/0"] vias=["2/0"]
structdevice.connect_nets cell="BLOCK" route_layer="3/0" route_width_um=0.5
structdevice.lvs_check cell="BLOCK" mode="both"
structdevice.spec_write cell="BLOCK" layer_roles={"1/0":"gate","3/0":"metal1"}
9 · Nanodevices (Hall bar / EBL / flake) device_nanodevice · 2 tools
nanodevice.hallbar is a one-call closed loop: from a HallBarSpec it computes and draws the whole device (bar + N contact arms + pads + Port markers + labels), then delegates routing to the generic router (overlap validation on, optional EBL writefield walls as keepouts), committing to a disposable cell. Failures return problems/next_action and change nothing.
| Tool | Params | Function |
|---|---|---|
nanodevice.hallbar | cell, spec, writefield, route_layer, spacing_um, dry_run, session | Build + route + validate + commit one Hall bar in one call. |
nanodevice.detect_commit | cell, traces_path, image, pixel_size_um, coordinate, dry_run, session | Commit flake traces as polygons. traces_path (no extra deps) or image+pixel_size_um for live detection (needs cv2 + numpy). |
nanodevice.hallbar cell="HB1" spec={"bar_length_um":60,"bar_width_um":8,"contact_count":6,"pad_size_um":40,"pitch_um":24,"gap_um":6} route_layer="1/0" dry_run=true
nanodevice.detect_commit cell="FLAKE" traces_path="out/traces.json"
10 · Photonics (gdsfactory import / connect / reroute) device_photonics · 4 tools
Photonic circuit flow; needs gdsfactory in the interpreter. photonics.import_gf takes over a finished gdsfactory script in one call; port.harvest_blackbox derives Ports from live blackbox positions via the stub convention. After takeover, drag a component in the GUI → one photonics.reroute re-routes optics and metal together. That drag → reroute loop is the point: the layout stays live and editable.
| Tool | Params | Function |
|---|---|---|
photonics.import_gf | script_path*, component, cell, route_layer, port_layer, route, session | Take over a finished gdsfactory script: import device instances (batch RPC), collapse routed connections to nets, persist port templates + net table, route with klink. |
port.harvest_blackbox | cell*, tags*, nets, wg_layer*, stub_size_um*, port_layer, clear | Derive Ports from PDK blackbox instances via the waveguide stub convention. Re-run after moving instances. |
photonics.connect | recent_sends*, cell, width_um, radius_um, separation_um, wg_layer, stub_size_um, route_layer | Connect ports the user just SENT: read latest N SENDs → port pairs → auto-name nets → persist → re-harvest → route. |
photonics.reroute | cell*, session, wg_layer, stub_size_um, route_layer | Re-route after the user moved components (reads the persisted net table). Multi-port optical nets need a splitter first. |
photonics.import_gf script_path="my_mzi.py" cell="MZI" route_layer="1/0"
# drag a component in KLayout, then:
photonics.reroute cell="MZI"
port.harvest_blackbox cell="MZI" tags=["gc","mmi"] wg_layer="1/0" stub_size_um=0.5
photonics.connect recent_sends=4 cell="MZI" radius_um=10 separation_um=5
11 · Escape hatch (pya exec, events, recorder) escape_hatch · 8 tools
Prefer typed RPCs. exec.python runs raw pya for what no typed RPC covers — it still schedules recorder + layout-diff detection. events.* is the live event stream (usually read interaction.* instead). recorder.* generates a replay script; check recorder.status before tests so you never clobber a user recording.
| Tool | Params | Function |
|---|---|---|
exec.python mutates | (code + optional reset) | Run arbitrary Python on KLayout's Qt main thread. Pre-bound pya, mw, view, layout. State persists per connection (reset=true wipes). stdout/stderr captured. |
exec.reset mutates | — | Clear the per-connection namespace. |
events.channels | — | List pushable event channels (NDJSON frames). |
events.status | — | Subscription + SignalHub diagnostics. |
events.subscribe | channels* | Subscribe to channels (unknown silently ignored). |
events.unsubscribe | channels | Unsubscribe; * drops all. |
recorder.start | output_path | Begin recording into a replayable Python script. Idempotent. |
recorder.stop | output_path | Stop and write the script; returns stats + wrote. |
recorder.status | — | Recording state, event/action counts, output path. |
recorder.status
recorder.start
# … typed RPC edits + manual GUI edits …
recorder.stop # writes .py and standalone _pya.py
exec.python code="print(layout.top_cell().name)"