mjEdit ist nicht nur ein OSCAL-Editor, sondern eine Plattform. Das gesamte Programm ist auf einem dokumentierten Plugin-System aufgebaut: Selbst zentrale Funktionen wie der OSCAL-Editor, die Netzwerk-Discovery oder der MCP-Server liegen als Plugins vor und nutzen genau die API, die auch Ihnen offensteht.

Wenn ein internes mjEdit-Feature damit umsetzbar war, ist Ihr Plugin es auch.

Das Besondere

  • Open by design: mjEdit ist als erweiterbare Anwendung konzipiert. Funktionen wie OSCAL-Tabs, Browser-Tab, Database-Tab, Netzwerk-Discovery, MCP-Server und JSON-Transform-Tools sind eigene Plugins – kein zugeklemmtes Closed-Source-Innenleben.
  • Stabile Hook-Verträge: Die Schnittstellen sind in plugins/hook_contracts.py als Enum + Dataclass-Events versioniert. Aufrufe wie file_opened werden über ein typisiertes FileOpenedEvent zugestellt – alte Signaturen bleiben abwärtskompatibel.
  • Lifecycle-Trennung: Frühes on_load() für Registrierungen, separates on_gui_ready() sobald die GUI vollständig steht. Das verhindert die typischen „MainGUI noch nicht da"-Crashes anderer Plugin-Systeme.
  • Robust isoliert: Fehler in einem Plugin-Hook blockieren weder den Core noch andere Plugins. Beim Entladen werden Menü-Items, Toolbar-Buttons, Editor-Funktionen und Hooks automatisch durch BasePlugin.on_unload() aufgeräumt.
  • Konfiguration statt Klick-Installation: Aktivierung über config/config.json → sys_active_plugins. Versionsfest, deploybar, Git-freundlich.

Wie es funktioniert

plugins/
├── __init__.py          # PluginManager: Laden, Aktivieren, Hook-Aufrufe, Entladen
├── base.py              # BasePlugin: Lifecycle + Menü-/Toolbar-Helfer + Cleanup
├── hook_contracts.py    # HookName-Enum + typisierte Events
└── my_plugin/
    ├── __init__.py      # exportiert Plugin
    └── plugin.py        # Ihre Plugin-Klasse

Jedes Plugin exportiert eine Klasse Plugin, die von BasePlugin erbt. Der PluginManager lädt nur Plugins, die in sys_active_plugins aufgelistet sind, ruft den Lifecycle in der richtigen Reihenfolge auf und verteilt Hook-Aufrufe an alle registrierten Callbacks.

Minimalbeispiel – ein Plugin in unter 30 Zeilen

from plugins.base import BasePlugin, PluginType
from utils.i18n import _


class Plugin(BasePlugin):
    name = "Mein Plugin"
    version = "1.0.0"
    description = "Beispiel für die mjEdit Plugin-API"
    author = "Ihr Name"

    def __init__(self):
        super().__init__()
        self.plugin_type = PluginType.EDITOR_PLUGIN

    def on_load(self):
        self.add_menu_item(_("Mein Menüpunkt"), self.show_message)
        self.register_hook("file_opened", self.on_file_opened)

    def on_gui_ready(self):
        main_gui = self.manager.main_gui
        main_gui.widgets.set_status(_("Mein Plugin ist bereit"), timeout=3000)

    def show_message(self, main_gui):
        self.show_info(_("Mein Plugin wurde aufgerufen."))

    def on_file_opened(self, file_path, content, is_large_file=False):
        self.log(f"Datei geöffnet: {file_path}")

Aktivieren mit einem Eintrag in config/config.json:

{ "sys_active_plugins": ["my_plugin"] }

Fertig. Beim nächsten Start steht Ihr Menüpunkt im Plugins-Menü, Ihr file_opened-Handler reagiert auf jede geöffnete Datei.

Plugin-Typen

Drei Grundtypen über PluginType:

Typ Wofür Beispiele aus dem Core
EDITOR_PLUGIN Editor erweitern: Menüs, Funktionen, Hook-Reaktionen transform_script_plugin, network_discovery_plugin
GUI_PLUGIN Eigene Tabs, Dialoge, Fenster oscal_plugin, browser_plugin, database_plugin
TOOL_PLUGIN Hintergrund-Werkzeuge ohne eigene UI mje_mcp_server_plugin, gui_auto_test_plugin

Was lässt sich konkret bauen?

Die im Lieferumfang enthaltenen Plugins zeigen die Bandbreite – jedes davon ist ein realistisches Vorbild für eigene Erweiterungen:

  • Eigene Editor-Tabs für Domänen-spezifische Dateiformate (analog zum OSCAL-Plugin mit 8 spezialisierten Editoren).
  • Externe Tools andocken – ein Plugin kann eigene Server starten (siehe mje_mcp_server_plugin, das einen kompletten MCP-Server mit 154 Tools registriert).
  • Datenbank-Workbenches als Tab (siehe database_plugin).
  • Netzwerk- und Inventar-Tools, die ihre Ergebnisse direkt in geöffnete OSCAL-Dokumente schreiben (siehe network_discovery_plugin).
  • Transformationen und Auto-Repair für JSON-Strukturen (siehe transform_script_plugin).
  • Webbrowser oder externe Viewer als integrierten Tab (siehe browser_plugin).
  • Test- und Automatisierungs-Plugins, die GUI-Aktionen scripten (siehe gui_auto_test_plugin).
  • File-Type-Reaktoren, die auf file_opened / file_saved / file_renamed lauschen und z. B. Validierung, Konvertierung oder externes Logging anstoßen.

Hook-Referenz (Auszug)

Hook Signatur Zweck
add_menu (menubar, main_gui) Menüleiste erweitern
add_toolbar (toolbar, main_gui) Toolbar erweitern
file_opened FileOpenedEvent auf geöffnete Dateien reagieren
file_saved (file_path, main_gui) nach dem Speichern reagieren
file_renamed (old_path, new_path) Umbenennungen verarbeiten
file_save_requested (file_path) Speichern selbst übernehmen (return True)
save_active_plugin_tab (tab_index) aktiven Plugin-Tab für Strg+S speichern
get_plugin_file_path (tab_index) Dateipfad eines Plugin-Tabs liefern
open_external_url (url) externe URL plugin-intern behandeln
on_tab_changed (tab_index, tab_name) auf Tabwechsel reagieren

Domänenspezifische Hooks (z. B. add_excel_resource_to_oscal, update_oscal_resource_base64) sind über das OSCAL-Plugin verfügbar und werden nur dort aktiv – Ihre Plugins können sich gezielt einklinken.

Vorteile für Entwickler

  • Schnell zum ersten Erfolg: Erstes lauffähiges Plugin in einer halben Stunde – example_plugin als kopierbarer Startpunkt liegt im Repository.
  • Echte API, keine Fassade: Sie nutzen exakt dieselben Hooks und Helfer, mit denen das Core-Team selbst Tabs, Menüs und Tools baut.
  • PySide6 + Python: Full Qt power, familiar Python stack, no own DSL.
  • Clean separation: BasePlugin delivers secure cleanup, i18n via utils.i18n._(), uniform logging and error dialogs without boilerplate.
  • Stable contracts: HookName enum + dataclass events mean: Refactorings in core don’t silently break your plugin.
  • Good examples: Eight plugins included in the repository cover almost every extension case - from tab GUI to background server.
  • Documentation and testing: doc_dev/PLUGINS_DEV.md is the official, maintained reference. Plugins are testable like normal Python packages.
  • No marketplace hurdle: You deliver your plugin as a directory - no store, no signature, no approval pipeline.

License question – AGPL and your plugin

mjEdit is available under the GNU Affero General Public License v3 (AGPL-3.0). This has clear consequences as soon as you write a plugin that uses the mjEdit API:

What AGPL means for plugin developers

  1. Plugins are a derivative work. Since your plugin is based directly on BasePlugin, the hook contracts and the internal API of mjEdit (from plugins.base import BasePlugin), a derivative work is created in the copyright sense. This means that the copyleft clause of the AGPL applies.
  2. Your plugin must also be AGPL compatible. In practice: AGPL-3.0 or an explicitly compatible license. Proprietary / closed source is not permitted as soon as you make your plugin available to third parties or operate it as a service.
  3. Source code provision is mandatory - both when passing on the binaries (classic GPL obligation) and for network provision (the AGPL special feature compared to GPL). Anyone who offers mjEdit + your plugin as a service must make the source code of all parts accessible.
  4. Internal use within the company is not critical. As long as the plugin is only used internally and is not distributed to third parties or offered as a network service, there are no publication obligations.
  5. Commercial use is permitted. AGPL ≠ “non-commercial”. You can sell plugins, offer support, offer consulting services related to your plugin - you just have to provide the source code or make it accessible.
  6. Headers and license text. Adopt the AGPL header that all core files also carry (see plugins/base.py) and include a LICENSE file (or COPYING).

Effects in practice

Scenario Consequence
Only use the plugin internally No publication requirement – ​​AGPL requires nothing.
Pass the plugin on to customers Source code of the plugin must be supplied as AGPL-3.0.
mjEdit + plugin as SaaS / web service Source code of all parts must be accessible to users of the service (network clause of the AGPL).
Publish plugin on GitHub / GitLab License notice AGPL-3.0 + headers in all source files.
Sell ​​closed source plugin Not possible without a separate commercial license from the mjEdit rights holder.

If you need closed source

If you want to write a plugin that cannot be published for business reasons - for example because it contains proprietary algorithms or customer data schemas - a commercial dual license for mjEdit is fundamentally not possible and also non-negotiable, since mjEdit itself is based on GPL or AGPL licenses. Please contact us using the contact form so ​​that together we can find a solution to your requirements - be it through advice, development assistance or the possibility of publishing your plugin under AGPL.

Recommendation

For most plugin developers, AGPL is an advantage, not a hindrance: your plugin benefits from a stable, openly maintained editor core; Users gain trust through open sources; Auditors and authorities strongly prefer AGPL software in compliance environments. If you develop a plugin based on the mjEdit API, we strongly recommend releasing it under AGPL-3.0 as well - this way everyone benefits from the openness and extensibility of the platform.

Getting started

  1. Clone repository.
  2. Copy plugins/example_plugin/ as a template into plugins/my_plugin/.
  3. Expand config/config.json with "my_plugin" in sys_active_plugins.
  4. Fill plugin class (on_load, on_gui_ready, desired hooks).
  5. Start mjEdit – your menu item appears in the plugins menu.
  6. Read developer guide: doc_dev/PLUGINS_DEV.md.

Best practices- GUI strikt vom Load trennen – Hook-Registrierung in on_load(), GUI-Erzeugung erst in on_gui_ready().

  • Globale Shortcuts nicht doppeln – Strg+S, Strg+W etc. werden zentral behandelt; nutzen Sie save_active_plugin_tab statt eigener Shortcuts.
  • Fehler isolieren – Hook-Handler defensiv schreiben; benutzerrelevante Fehler über self.show_error(...).
  • i18n verwenden – sichtbare Texte über utils.i18n._() führen.
  • Lazy Imports – schwere GUI-Abhängigkeiten erst beim ersten Aufruf importieren.
  • Eigene Doku im Plugindoc_dev/ und doc_user/ direkt im Plugin-Verzeichnis pflegen.

Sie möchten ein Plugin entwickeln?

Wir unterstützen Plugin-Autoren mit API-Beratung, Code-Reviews, AGPL-Konformitätsprüfung. Schreiben Sie uns.