Guide · Extension Packages

Writing a klink extension package (klink.plugins)

klink ships mechanism only; your process data and house tools can live in your own pip package and plug in through the klink.plugins entry point, without modifying a line of klink.

1. Why write an extension package

klink ships mechanism; your process data and house tools can live in your own pip package and plug in without modifying klink. A lab or PDK vendor publishes one package; anyone who installs it next to klayout-klink gets the vendor's tools in klink.find_tools, and the vendor's named profiles / device libraries / recipes / stacks resolvable by name.

2. The minimal working package

Two files. pyproject.toml:

[project]
name = "acme-pdk-klink"
version = "0.1.0"
dependencies = ["klayout-klink"]

[project.entry-points."klink.plugins"]
acme = "acme_pdk.klink_ext:register"

acme_pdk/klink_ext.py:

from acme_pdk.process import ACME_2M_PROFILE     # your data, your package

def hello(ctx, arguments):
    # ctx is the MCP bridge: ctx._client reaches the live KLayout session
    return {"greeting": f"hello {arguments.get('who', 'world')}"}

def register(hook):
    hook.add_domain(
        "acme_pdk", title="ACME PDK",
        summary="ACME's process data and helpers",
        usage="Resolve the profile with klink.ext.get_resource('profile', "
              "'acme_2m'); acme_pdk.hello is a smoke check.")
    hook.add_tool(
        "acme_pdk.hello", hello,
        description="smoke-check the ACME extension",
        input_schema={"type": "object",
                      "properties": {"who": {"type": "string"}}},
        domain="acme_pdk")
    hook.add_profile("acme_2m", ACME_2M_PROFILE)

pip install acme-pdk-klink next to klink, restart the MCP server, and:

  • klink.find_tools (no args) lists an acme_pdk domain with its tool count; domain="acme_pdk" returns your usage text — your extension gets the same progressive disclosure as built-in domains.
  • acme_pdk.hello is callable like any other tool.
  • klink.ext.get_resource("profile", "acme_2m") returns your ProcessProfile from any example or script.

3. What you can contribute

hook callcontributesnotes
add_tool(name, handler, description=, input_schema=, domain=)an MCP toolname MUST be namespaced <token>.<name>; built-in prefixes (view., shape., routing., …) are reserved and rejected
add_domain(token, title=, summary=, usage=)a find_tools domainthe usage string is your skill-like recipe, disclosed on demand
add_profile(name, obj) / add_devices / add_recipe / add_stacknamed resourcesresolved via klink.ext.get_resource(kind, name); how examples and flows pick up your process data

Handlers follow the local-tool contract: handler(ctx, arguments) returning a JSON-serializable dict; ctx is the MCP bridge (its _client is the connected KLinkClient). Contributed tools are expected to follow the same design rules as built-ins — one user intention per call, errors that instruct, validate before mutating.

4. Fault isolation (what happens when a package is broken)

Discovery is lazy (first use) and per-package isolated:

  • an extension that fails to import or whose register() raises is rolled back completely (no half-registered tools) and recorded as a failure naming the package and the error;
  • other extensions and every built-in tool are unaffected — the MCP server never crashes because of a broken extension;
  • klink.status reports the extensions block: installed packages with their contributions, and the failure list. Check it first when an expected tool is missing.
  • With zero extensions installed, klink's tool list is byte-identical to a plain install — the mechanism costs nothing until used.

5. Verifying your package

  1. pip install -e . your extension next to klayout-klink.
  2. klink.status → your package under extensions.installed, empty failures.
  3. klink.find_tools → your domain appears with the right count; call your tool once.
  4. python -c "from klink import ext; print(ext.get_resource('profile', '<name>'))"
  5. Uninstall and re-check: everything disappears cleanly.

Process purity applies to you too: keep the numbers (layers, dimensions, z tables) in your package's data modules, and pass them explicitly into klink APIs — your extension is exactly the "example that owns its process" pattern, shipped as a distribution.