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.pyals Enum + Dataclass-Events versioniert. Aufrufe wiefile_openedwerden über ein typisiertesFileOpenedEventzugestellt – alte Signaturen bleiben abwärtskompatibel. - Lifecycle-Trennung: Frühes
on_load()für Registrierungen, separateson_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_renamedlauschen 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_pluginals 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:
BasePlugindelivers secure cleanup, i18n viautils.i18n._(), uniform logging and error dialogs without boilerplate. - Stable contracts:
HookNameenum + 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.mdis 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
- 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. - 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.
- 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.
- 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.
- 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.
- Headers and license text. Adopt the AGPL header that all core files also carry (see
plugins/base.py) and include aLICENSEfile (orCOPYING).
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
- Clone repository.
- Copy
plugins/example_plugin/as a template intoplugins/my_plugin/. - Expand
config/config.jsonwith"my_plugin"insys_active_plugins. - Fill plugin class (
on_load,on_gui_ready, desired hooks). - Start mjEdit – your menu item appears in the plugins menu.
- 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_tabstatt 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 Plugin –
doc_dev/unddoc_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.