Skip to content

Packaging and installing

A Python plugin is a folder with a plugin.json and a Python module. You can install one two ways: package it as a .mteplugin archive and use the editor's installer (recommended), or drop the folder into a plugins directory by hand.

Start from the template

A ready-to-copy starter lives at Python/templates/plugin-template-python/ in the repo — three files (plugin.json, main.py, README.md) plus a Claude-facing CLAUDE.md. Rename the folder to your plugin's short name, edit four fields in plugin.json, and start writing register(ctx). The installed SDK ships the same template under share/MTEPluginSDK/plugin-template-python/.

Plugin folder layout

hello/
├── plugin.json      # required
└── main.py          # required; module name from plugin.json.module

Everything the plugin needs must sit inside this folder. Sub-imports work but must be flat — dotted module names in plugin.json are a future extension.

You may add extra files (README, LICENSE) — the host ignores what it doesn't recognise.

Zip the plugin's files and give the archive the .mteplugin extension (a plain .zip also works). Both shapes are accepted:

hello.mteplugin              hello.mteplugin
├── plugin.json              └── hello-1.0.0/        # any folder name
└── main.py                      ├── plugin.json
                                 └── main.py

The single wrapper folder — the natural result of zipping a folder — is normalised away: the plugin always installs to <Plugins>/<id>/, keyed by the manifest id, never by the archive's folder name. That is also why installing version 2 of a plugin replaces version 1 instead of sitting next to it.

Install it via Preferences → Plugins → Install… and restart the editor.

Only install packages you trust

The installer checks that a package is structurally sound (see below) — it does not make an untrusted plugin safe to run. Once loaded, a plugin runs with the same access to your files, settings and network as the editor, and is not sandboxed. The static register(...) probe runs no code at install time, but the plugin's code does run on the next launch. Treat a .mteplugin (and any config or JSON files it ships) like an unknown program: install it only from a source you trust. See Plugins for the full warning.

What the installer validates

The package is fully validated against a throwaway copy before anything on disk is touched — a bad package can never disturb an existing install:

  1. plugin.json parses, has an id, apiVersion matches the editor, entry is "python", and module is present.
  2. The archive listing is safe: no absolute or .. entry paths, at most 10 000 entries, at most 256 MiB unpacked, and at least one .py file.
  3. After extraction to a scratch directory, the declared <module>.py exists and declares a top-level def register(...) — the same static probe discovery uses. No Python code is executed at install time.

Any failure produces a human-readable error dialog and leaves the editor's plugins untouched.

Updating and uninstalling

  • Installing a package whose id is already installed replaces it. If that plugin is currently loaded, the update is staged and applied on the next launch.
  • Uninstall from the same Preferences page; removal also happens at the next launch, while nothing is loaded.
  • Bundled (built-in) plugins cannot be overridden by an installed package with the same id.

Installing by hand (folder drop)

The editor discovers plugins in two directories on every startup:

  1. Bundled — next to the executable, at <exe-dir>/Plugins/. Built-in plugins (e.g. HelloPython) live here.
  2. User — per-user, writable. On Windows this is %LOCALAPPDATA%/MTE/Plugins/, on macOS ~/Library/Application Support/MTE/Plugins/, on Linux ~/.local/share/MTE/Plugins/.

Both directories are scanned. If the same id appears in both, the first occurrence wins (bundled takes precedence). Restart the editor to pick up new folders — hot reload is not supported.

Two acceptable layouts

The scan looks for plugin.json either directly in the plugins directory (loose) or inside one level of immediate subdirectory (per-plugin folder):

Plugins/
├── plugin.json         # loose -- one plugin at the top level
├── main.py
└── hello/              # per-plugin folder -- one plugin
    ├── plugin.json
    └── main.py

The per-plugin folder is the recommended layout; it's the shape the installer produces.

Dependencies (pyRequires)

plugin.json may declare pip requirements as an inline array:

"pyRequires": ["requests>=2.31"]

On the plugin's first launch the editor provisions a virtual environment at <AppLocalData>/PluginVenv/<id>/ (python -m venv + pip install, which needs network access that one time) and runs the plugin's worker under that venv's interpreter. Subsequent launches reuse it; changing pyRequires re-provisions automatically (the venv carries a stamp of the requirements it was built for).

  • Provisioning failures (no network, a typo'd requirement) leave the plugin listed but not loaded, with the pip error in the log; the next launch retries.
  • Uninstalling a plugin removes its venv along with the plugin folder (a venv is derived data — it is rebuilt from pyRequires if you ever reinstall).

Offline / locked-down machines

Ship a pre-built venv inside the package as a venv/ folder at the plugin root. When present it wins over everything — no pip ever runs. Build it on a machine with the same OS/architecture, since venvs are not portable across platforms.

Vendoring (small pure-Python deps)

For a single pure-Python module, vendoring inside the plugin folder still works and avoids the venv machinery entirely:

# main.py
import os, sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "vendor"))

import your_dep

Declared permissions

plugin.json may carry a permissions array (e.g. ["network"]). The Plugins Preferences page shows it in a read-only Permissions column so users can see what a plugin declares about itself. It is a declaration, not an enforcement — sandboxing is future work.

Where writable data goes

The plugins directory holds code. Runtime data (caches, per-user configuration, indexes) go to a separate per-plugin data directory the host manages — ctx.data_dir() returns its absolute path. Do NOT write into the plugins directory from your .py at runtime; uninstall removes the whole <Plugins>/<id>/ folder, and writes made elsewhere in the plugins tree are never cleaned up.

Deploying to other machines

Ship the .mteplugin file — it is self-contained and validated on the receiving end. A zip/tarball/git-clone of the folder into the per-user plugins directory works too.

Enable / disable

The Plugins Preferences page lists every discovered plugin (native + Python, together) with the same origin/status columns. Disabling a plugin keeps it listed but prevents its factory from being invoked at startup — no worker is spawned. Re-enable and restart to bring it back.