KLayout Observer MCP Implementation Plan¶
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Build a Python-only, observer-first MCP server for KLayout that can open PIC layouts, inspect geometry, render deterministic images, and run existing DRC decks with machine-readable outputs.
Architecture: Implement one Python package with a thin MCP adapter over a KLayout bridge. The bridge owns session state, geometry queries, rendering, and DRC integration; the MCP layer owns input validation, tool registration, and error normalization. Headless deterministic mode is the only required runtime mode in MVP.
Tech Stack: Python 3.11+, official Python MCP SDK, klayout.db, klayout.lay, external klayout CLI for batch DRC, pytest, pytest-cov, ruff
Repository precondition: If this directory is not already a git repository, run git init before Task 1 so the commit checkpoints are valid.
Task 1: Bootstrap the repository¶
Status: Completed
Files:
- Create: README.md
- Create: pyproject.toml
- Create: src/klayout_mcp/__init__.py
- Create: src/klayout_mcp/server.py
- Create: tests/conftest.py
- Create: tests/test_server_smoke.py
Step 1: Write the failing smoke test
from klayout_mcp.server import build_server
def test_build_server_exposes_expected_tool_names():
server = build_server()
tool_names = {tool.name for tool in server.list_tools()}
assert "open_layout" in tool_names
assert "render_view" in tool_names
Step 2: Run test to verify it fails
Run: pytest tests/test_server_smoke.py -q
Expected: FAIL with import or attribute errors because the package does not exist yet.
Step 3: Write minimal implementation
EXPECTED_TOOLS = [
"open_layout",
"close_session",
"list_cells",
"describe_cell",
"list_layers",
"query_region",
"measure_geometry",
"set_view",
"render_view",
"run_drc_script",
"extract_markers",
]
Expose build_server() from src/klayout_mcp/server.py and make the smoke test pass with placeholder tool registration.
Step 4: Run test to verify it passes
Run: pytest tests/test_server_smoke.py -q
Expected: PASS
Step 5: Commit
git add README.md pyproject.toml src/klayout_mcp/__init__.py src/klayout_mcp/server.py tests/conftest.py tests/test_server_smoke.py
git commit -m "chore: bootstrap klayout mcp package"
Task 2: Add configuration, errors, and session storage¶
Status: Completed
Files:
- Create: src/klayout_mcp/config.py
- Create: src/klayout_mcp/errors.py
- Create: src/klayout_mcp/models.py
- Create: src/klayout_mcp/session_store.py
- Create: tests/test_config.py
- Create: tests/test_session_store.py
Step 1: Write the failing tests
from pathlib import Path
from klayout_mcp.config import Settings
from klayout_mcp.session_store import SessionStore
def test_default_artifact_root_is_repo_local(tmp_path: Path):
settings = Settings.from_root(tmp_path)
assert settings.artifact_root == tmp_path / ".artifacts"
def test_session_store_closes_and_deletes_artifacts(tmp_path: Path):
store = SessionStore(tmp_path, ttl_seconds=3600)
session = store.create_dummy_session()
assert store.close(session.session_id)["closed"] is True
Step 2: Run tests to verify they fail
Run: pytest tests/test_config.py tests/test_session_store.py -q
Expected: FAIL because settings and session store are not implemented.
Step 3: Implement minimal settings and store
@dataclass(slots=True)
class Settings:
artifact_root: Path
session_ttl_seconds: int
klayout_bin: str
Implement: - env parsing - artifact directory creation - lazy TTL cleanup - explicit close semantics
Step 4: Run tests to verify they pass
Run: pytest tests/test_config.py tests/test_session_store.py -q
Expected: PASS
Step 5: Commit
git add src/klayout_mcp/config.py src/klayout_mcp/errors.py src/klayout_mcp/models.py src/klayout_mcp/session_store.py tests/test_config.py tests/test_session_store.py
git commit -m "feat: add config and session storage"
Task 3: Create programmatic layout fixtures¶
Status: Completed
Files:
- Create: tests/fixtures/layout_factory.py
- Create: tests/test_layout_factory.py
Step 1: Write the failing fixture test
from tests.fixtures.layout_factory import build_waveguide_fixture
def test_waveguide_fixture_has_expected_top_cell_and_layers(tmp_path):
fixture = build_waveguide_fixture(tmp_path)
assert fixture.top_cell == "TOP"
assert fixture.path.exists()
Step 2: Run test to verify it fails
Run: pytest tests/test_layout_factory.py -q
Expected: FAIL because the fixture factory does not exist.
Step 3: Implement the minimal fixture factory
Generate at least these synthetic layouts through KLayout APIs: - straight waveguide - simple bend - directional coupler - hierarchical two-cell layout - text-label fixture for port spacing tests
Return a small metadata object with:
- path
- top_cell
- expected_layers
- expected_bbox_um
Step 4: Run test to verify it passes
Run: pytest tests/test_layout_factory.py -q
Expected: PASS
Step 5: Commit
git add tests/fixtures/layout_factory.py tests/test_layout_factory.py
git commit -m "test: add generated klayout fixtures"
Task 4: Implement open_layout, close_session, and list_layers¶
Status: Completed
Files:
- Create: src/klayout_mcp/bridge/layout_loader.py
- Create: src/klayout_mcp/tools/layout_tools.py
- Modify: src/klayout_mcp/server.py
- Create: tests/test_open_layout.py
- Create: tests/test_list_layers.py
Step 1: Write the failing tests
def test_open_layout_returns_session_and_bbox(mcp_client, generated_layout):
result = mcp_client.call("open_layout", {"path": str(generated_layout.path)})
assert result["session_id"].startswith("ses_")
assert result["selected_top_cell"] == generated_layout.top_cell
def test_list_layers_reports_numeric_layers(mcp_client, opened_session):
result = mcp_client.call("list_layers", {"session_id": opened_session})
assert result["layers"][0]["layer"] >= 0
Step 2: Run tests to verify they fail
Run: pytest tests/test_open_layout.py tests/test_list_layers.py -q
Expected: FAIL because these tools do not exist yet.
Step 3: Implement minimal tools
layout_loader.py should:
- validate absolute path
- infer format
- load layout through klayout.db.Layout
- compute top cells, dbu, bbox, and layer list
layout_tools.py should:
- open the session
- close the session
- list layers in deterministic order
Step 4: Run tests to verify they pass
Run: pytest tests/test_open_layout.py tests/test_list_layers.py -q
Expected: PASS
Step 5: Commit
git add src/klayout_mcp/bridge/layout_loader.py src/klayout_mcp/tools/layout_tools.py src/klayout_mcp/server.py tests/test_open_layout.py tests/test_list_layers.py
git commit -m "feat: add layout open and layer inspection tools"
Task 5: Implement list_cells and describe_cell¶
Status: Completed
Files:
- Create: src/klayout_mcp/bridge/hierarchy.py
- Create: tests/test_list_cells.py
- Create: tests/test_describe_cell.py
- Modify: src/klayout_mcp/tools/layout_tools.py
- Modify: src/klayout_mcp/server.py
Step 1: Write the failing tests
def test_list_cells_returns_sorted_cell_names(mcp_client, opened_hierarchical_session):
result = mcp_client.call("list_cells", {"session_id": opened_hierarchical_session, "max_depth": 2})
names = [cell["name"] for cell in result["cells"]]
assert names == sorted(names)
def test_describe_cell_returns_instance_and_label_data(mcp_client, opened_hierarchical_session):
result = mcp_client.call("describe_cell", {"session_id": opened_hierarchical_session, "cell": "TOP", "depth": 1})
assert "instances" in result
assert "shape_counts_by_layer" in result
Step 2: Run tests to verify they fail
Run: pytest tests/test_list_cells.py tests/test_describe_cell.py -q
Expected: FAIL because hierarchy helpers are not implemented.
Step 3: Implement minimal hierarchy support
Support: - sorted cell enumeration - child instance counting - bbox collection - depth-limited description - label extraction - simple transform serialization
Use plain dictionaries or Pydantic models, but the serialized field names must match the contract.
Step 4: Run tests to verify they pass
Run: pytest tests/test_list_cells.py tests/test_describe_cell.py -q
Expected: PASS
Step 5: Commit
git add src/klayout_mcp/bridge/hierarchy.py src/klayout_mcp/tools/layout_tools.py src/klayout_mcp/server.py tests/test_list_cells.py tests/test_describe_cell.py
git commit -m "feat: add cell hierarchy inspection tools"
Task 6: Implement query_region¶
Status: Completed
Files:
- Create: src/klayout_mcp/bridge/query.py
- Create: tests/test_query_region.py
- Modify: src/klayout_mcp/tools/layout_tools.py
- Modify: src/klayout_mcp/models.py
- Modify: src/klayout_mcp/server.py
Step 1: Write the failing tests
def test_query_region_returns_shape_refs(mcp_client, opened_session):
result = mcp_client.call(
"query_region",
{
"session_id": opened_session,
"box": {"left": 0.0, "bottom": 0.0, "right": 50.0, "top": 20.0},
"hierarchy_mode": "recursive",
},
)
assert result["shapes"]
assert result["shapes"][0]["id"].startswith("shp_")
def test_query_region_reports_truncation(mcp_client, opened_dense_session):
result = mcp_client.call(
"query_region",
{
"session_id": opened_dense_session,
"box": {"left": 0.0, "bottom": 0.0, "right": 500.0, "top": 500.0},
"max_shapes": 1,
},
)
assert result["truncation"]["shapes_dropped"] >= 0
Step 2: Run tests to verify they fail
Run: pytest tests/test_query_region.py -q
Expected: FAIL because region query and stable shape refs do not exist.
Step 3: Implement region query logic
Support:
- box validation
- layer filtering
- hierarchy modes: top, recursive, flattened
- deterministic shape ordering
- shape IDs stable within a session
- truncation metadata
Shape summaries should include:
- id
- kind
- cell
- instance_path
- layer
- bbox_um
- shape-specific lightweight stats such as point_count or path_width_um
Step 4: Run tests to verify they pass
Run: pytest tests/test_query_region.py -q
Expected: PASS
Step 5: Commit
git add src/klayout_mcp/bridge/query.py src/klayout_mcp/tools/layout_tools.py src/klayout_mcp/models.py src/klayout_mcp/server.py tests/test_query_region.py
git commit -m "feat: add bounded region queries"
Task 7: Implement measure_geometry¶
Status: Completed
Files:
- Create: src/klayout_mcp/bridge/measure.py
- Create: tests/test_measure_geometry.py
- Modify: src/klayout_mcp/tools/layout_tools.py
- Modify: src/klayout_mcp/server.py
Step 1: Write the failing tests
def test_measure_geometry_reports_path_width(mcp_client, queried_waveguide_region):
target_id = queried_waveguide_region["shapes"][0]["id"]
result = mcp_client.call("measure_geometry", {"session_id": queried_waveguide_region["session_id"], "mode": "path_width", "target_ids": [target_id]})
assert result["value_um"] > 0
def test_measure_geometry_reports_edge_gap_for_coupler(mcp_client, queried_coupler_region):
ids = [shape["id"] for shape in queried_coupler_region["shapes"][:2]]
result = mcp_client.call("measure_geometry", {"session_id": queried_coupler_region["session_id"], "mode": "edge_gap", "target_ids": ids})
assert result["value_um"] >= 0
Step 2: Run tests to verify they fail
Run: pytest tests/test_measure_geometry.py -q
Expected: FAIL because measurement logic does not exist.
Step 3: Implement minimal measurement modes
Implement:
- path_width
- segment_length
- centerline_distance
- edge_gap
- bend_radius_estimate
- overlap
Implement label_distance and port_spacing after shape-based modes are passing.
Use lightweight geometry methods first. Do not overfit to photonic semantics in MVP.
Step 4: Run tests to verify they pass
Run: pytest tests/test_measure_geometry.py -q
Expected: PASS
Step 5: Commit
git add src/klayout_mcp/bridge/measure.py src/klayout_mcp/tools/layout_tools.py src/klayout_mcp/server.py tests/test_measure_geometry.py
git commit -m "feat: add geometry measurement tools"
Task 8: Implement set_view and render_view¶
Status: Completed
Files:
- Create: src/klayout_mcp/bridge/render.py
- Create: tests/test_render_view.py
- Modify: src/klayout_mcp/tools/layout_tools.py
- Modify: src/klayout_mcp/session_store.py
- Modify: src/klayout_mcp/server.py
Step 1: Write the failing tests
from pathlib import Path
def test_render_view_writes_png(mcp_client, opened_session):
result = mcp_client.call(
"render_view",
{
"session_id": opened_session,
"box": {"left": 0.0, "bottom": 0.0, "right": 50.0, "top": 20.0},
"image_size": {"width": 800, "height": 600},
"style": "light",
},
)
assert Path(result["image"]["path"]).exists()
def test_set_view_updates_session_defaults(mcp_client, opened_session):
result = mcp_client.call(
"set_view",
{
"session_id": opened_session,
"box": {"left": 0.0, "bottom": 0.0, "right": 25.0, "top": 10.0},
},
)
assert result["view"]["box_um"]["right"] == 25.0
Step 2: Run tests to verify they fail
Run: pytest tests/test_render_view.py -q
Expected: FAIL because rendering and persisted view state do not exist.
Step 3: Implement rendering
Use klayout.lay.LayoutView with deterministic inputs:
- explicit cell
- explicit layer visibility
- explicit image size
- explicit bbox
Implementation notes:
- keep rendering isolated in one module
- persist updated view defaults into the session store
- if standalone rendering produces blank images, add the required view refresh or timer call before save_image_with_options
Step 4: Run tests to verify they pass
Run: pytest tests/test_render_view.py -q
Expected: PASS
Step 5: Commit
git add src/klayout_mcp/bridge/render.py src/klayout_mcp/tools/layout_tools.py src/klayout_mcp/session_store.py src/klayout_mcp/server.py tests/test_render_view.py
git commit -m "feat: add deterministic rendering tools"
Task 9: Implement run_drc_script and extract_markers¶
Status: Completed
Files:
- Create: src/klayout_mcp/bridge/drc.py
- Create: tests/test_run_drc_script.py
- Create: tests/test_extract_markers.py
- Create: tests/fixtures/drc/min_space.drc
- Modify: src/klayout_mcp/tools/layout_tools.py
- Modify: src/klayout_mcp/server.py
Step 1: Write the failing tests
def test_run_drc_script_returns_marker_summary(mcp_client, opened_violation_session, drc_script):
result = mcp_client.call(
"run_drc_script",
{
"session_id": opened_violation_session,
"script_path": str(drc_script),
"script_type": "ruby",
},
)
assert result["marker_count"] >= 1
def test_extract_markers_can_render_crops(mcp_client, completed_drc_run):
result = mcp_client.call(
"extract_markers",
{
"session_id": completed_drc_run["session_id"],
"run_id": completed_drc_run["run_id"],
"include_crops": True,
"crop_size_um": {"x": 20.0, "y": 20.0},
},
)
assert result["markers"][0]["crop"]["path"].endswith(".png")
Step 2: Run tests to verify they fail
Run: pytest tests/test_run_drc_script.py tests/test_extract_markers.py -q
Expected: FAIL because batch DRC integration and marker parsing do not exist.
Step 3: Implement DRC integration
Support:
- script path validation
- temp export of session layout into run directory
- batch invocation through klayout -b -r
- marker artifact discovery
- rule aggregation
- optional crop rendering around marker boxes
Keep stdout and stderr artifacts for debugging even on success.
Step 4: Run tests to verify they pass
Run: pytest tests/test_run_drc_script.py tests/test_extract_markers.py -q
Expected: PASS
Step 5: Commit
git add src/klayout_mcp/bridge/drc.py src/klayout_mcp/tools/layout_tools.py src/klayout_mcp/server.py tests/test_run_drc_script.py tests/test_extract_markers.py tests/fixtures/drc/min_space.drc
git commit -m "feat: add batch drc execution and marker extraction"
Task 10: Add integration coverage and polish¶
Status: Completed
Files:
- Create: tests/test_contract_smoke.py
- Create: tests/test_error_paths.py
- Modify: README.md
- Modify: docs/specs/2026-03-05-klayout-observer-mcp-contract.md
Step 1: Write the failing contract tests
def test_all_contract_tool_names_exist(mcp_client):
names = set(mcp_client.list_tool_names())
assert names == {
"open_layout",
"close_session",
"list_cells",
"describe_cell",
"list_layers",
"query_region",
"measure_geometry",
"set_view",
"render_view",
"run_drc_script",
"extract_markers",
}
def test_invalid_path_returns_contract_error_code(mcp_client):
result = mcp_client.call_expect_error("open_layout", {"path": "/tmp/missing.gds"})
assert result["code"] == "FILE_NOT_FOUND"
Step 2: Run tests to verify they fail
Run: pytest tests/test_contract_smoke.py tests/test_error_paths.py -q
Expected: FAIL until error normalization and contract parity are complete.
Step 3: Polish implementation and docs
Ensure: - all tool names match the contract - errors use exact codes - README explains local run flow - contract doc matches actual behavior - artifact paths are absolute
Step 4: Run the full suite
Run: pytest -q
Expected: PASS
Run: ruff check .
Expected: PASS
Step 5: Commit
git add README.md docs/specs/2026-03-05-klayout-observer-mcp-contract.md tests/test_contract_smoke.py tests/test_error_paths.py
git commit -m "chore: finalize contract coverage and docs"
Task 11: Manual verification¶
Status: Completed
Files: - No code changes expected
Step 1: Open a generated or real GDS
Run: python -m klayout_mcp.server
Expected: MCP server starts without crashing.
Step 2: Exercise the core flow manually
Call tools in this order:
- open_layout
- list_layers
- query_region
- measure_geometry
- render_view
Expected: - all responses are structured JSON - render PNG exists - measurements report both microns and dbu values
Step 3: Exercise one DRC run
Call:
- run_drc_script
- extract_markers
Expected:
- .lyrdb or machine-readable marker artifact exists
- marker summaries are returned
- crop images exist when requested
Step 4: Record any mismatches
Update:
- README.md
- docs/specs/2026-03-05-klayout-observer-mcp-contract.md
Only if behavior differs for defensible reasons.
Step 5: Final commit
git add README.md docs/specs/2026-03-05-klayout-observer-mcp-contract.md
git commit -m "docs: reconcile manual verification notes"