Skip to content

Plugin quickstart

Extend mklang with custom tools, hooks, machines, and providers — all via packaging entry points. No core changes, no monkey-patching.

The four plugin groups

Group What it adds Signature Entry in pyproject.toml
mklang.tools Host callables for tool: states (dict) → str below
mklang.hooks Code-hook gates (LLM-free predicates) (dict, Any) → bool below
mklang.machines Reusable .mkl machines dict or () → dict below
mklang.providers LLM adapter for a new provider subclass of LLM below

1. A custom tool

A tool is a (dict) → str callable that a tool: state invokes.

# pyproject.toml
[project]
name = "my-mklang-plugins"
version = "0.1.0"

[project.entry-points."mklang.tools"]
my_calculator = "my_plugins.tools:my_calculator"
# my_plugins/tools.py
def my_calculator(inp: dict) -> str:
    """Evaluate a math expression. Input: {"expr": "sqrt(2)"}.

    Returns a plain string — the observation deposited in context.
    """
    import math

    expr = inp.get("expr", "").strip()
    try:
        result = eval(expr, {"__builtins__": {}}, {"sqrt": math.sqrt, "pow": pow})
        return str(result)
    except Exception as e:
        return f"error: {e}"

Use it in a machine:

my_calc:
  tool: my_calculator
  input: { expr: "{{expression}}" }
  output: result
  gates:
    - when: otherwise
      then: ok
      to: END

Tool conventions

  • I/O tools (search, send, file write) should return the stub envelope (ADR 0020): {"tool": "name", "stub": false, "result": "…"}.
  • Pure offline tools (calc, date) can return a plain string.
  • Tools are host-provided — the .mkl never imports code.

2. A custom hook

A hook is a (context, output) → bool predicate that evaluates a gate without the LLM (ADR 0006).

[project.entry-points."mklang.hooks"]
is_business_hours = "my_plugins.hooks:is_business_hours"
# my_plugins/hooks.py
from datetime import datetime, timezone


def is_business_hours(ctx: dict, output: object) -> bool:
    """True when the current UTC hour is 9–17."""
    now = datetime.now(timezone.utc)
    return 9 <= now.hour < 17

Use it in a gate:

gates:
  - when: it is business hours
    hook: is_business_hours
    then: ok
    to: auto_approve
  - when: otherwise
    escalate: true
    to: human_review

3. A custom machine

A machine plugin registers a .mkl document (dict) or a zero-arg factory returning one.

[project.entry-points."mklang.machines"]
my_review = "my_plugins.machines:my_review_machine"
# my_plugins/machines.py
def my_review_machine():
    return {
        "machine": "my_review",
        "entry": "review",
        "budget": 6,
        "states": {
            "review": {
                "structure": "a one-line approval or rejection",
                "prompt": "Review: {{content}}",
                "output": "verdict",
                "gates": [
                    {"when": "the content is acceptable", "then": "ok", "to": "END"},
                    {"when": "otherwise", "repair": 1, "to": "review"},
                ],
            }
        },
    }

The machine is then runnable by name:

mklang run my_review --set content="hello world"

Or callable from another machine:

call: std_refine
input: { text: "{{draft}}" }
output: refined

Plugin precedence

The registry merges layers in this order (later wins):

stdlib ← plugins ← system ← user ← local

A plugin machine named std_research would shadow the bundled one — use distinct names to avoid surprises.

4. A custom provider

Provider plugins register an LLM adapter. The adapter must subclass mklang.llm.base.LLM.

[project.entry-points."mklang.providers"]
my_provider = "my_plugins.providers:MyProvider"
# my_plugins/providers.py
from mklang.llm.base import LLM


class MyProvider(LLM):
    """Adapter for a custom OpenAI-compatible endpoint."""

    def produce(self, prompt, *, guidance=None, policy=None, reason=False):
        # Return (reasoning_text_or_none, output_text)
        ...

    def judge(self, conditions, output, reasoning=None, context=None):
        # Return {"choice": k} where k is 1-based or N+1 for "none"
        ...

The reference interpreter ships adapters for OpenAI-compatible endpoints (openai_compat) and Anthropic (anthropic). A custom provider plugs in the same way.

5. Plugin policy

Plugins are subject to the MKLANG_ALLOWED_PLUGINS environment variable. If set, only plugin names in the comma-separated allowlist are loaded:

export MKLANG_ALLOWED_PLUGINS="my_calculator,my_review"

An empty value (the default) allows all plugins. This is a safety gate for production environments.

6. Distributing your plugin

# Build
pip install build
python -m build

# Publish
pip install twine
twine upload dist/*

Consumers install and get the entry points automatically:

pip install my-mklang-plugins
mklang machines   # your machine appears in the list

Reference: built-in plugins

Group Name Description
mklang.tools calc Safe arithmetic expression evaluator
mklang.tools search Web search (offline stub by default)
mklang.tools search_kb Knowledge-base lookup (stub)
mklang.tools send_reply Customer reply sender (stub)
mklang.tools list_files List workspace directory
mklang.tools read_file Read workspace file
mklang.tools write_file Write workspace file
mklang.hooks always_true Always returns True
mklang.hooks always_false Always returns False
mklang.hooks amount_le_100 Amount ≤ 100 (demo)
mklang.hooks has_receipt Receipt present (demo)
mklang.hooks auto_approve_ok Auto-approve logic (demo)
mklang.providers anthropic Anthropic API adapter

Parametric hooks (no plugin needed): eq:key:value, neq:key:value, write_failed.