mjEdit is not just an OSCAL editor, but a platform. The entire program is based on a documented plugin system: Even central functions such as the OSCAL editor, network discovery or the MCP server are available as plugins and use exactly the API that is also available to you.

If an internal mjEdit feature could be implemented with it, your plugin can too.

The special

  • Open by design: mjEdit is designed as an extensible application. Functions such as OSCAL tabs, browser tab, database tab, network discovery, MCP server and JSON transform tools are their own plugins - no closed-source inner workings.
  • Stable hook contracts: The interfaces are versioned in plugins/hook_contracts.py as Enum + Dataclass events. Calls like file_opened are delivered via a typed FileOpenedEvent - old signatures remain backwards compatible.
  • Lifecycle separation: Early on_load() for registrations, separate on_gui_ready() once the GUI is complete. This prevents the typical “MainGUI not there yet” crashes of other plugin systems.
  • Robustly isolated: Errors in a plugin hook do not block the core or other plugins. When unloading, menu items, toolbar buttons, editor functions and hooks are automatically cleaned up by BasePlugin.on_unload().
  • Configuration instead of click installation: Activation via config/config.json → sys_active_plugins. Version-proof, deployable, Git-friendly.

How it works

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

Each plugin exports a class Plugin, which inherits from BasePlugin. The PluginManager only loads plugins listed in sys_active_plugins, calls the lifecycle in the correct order, and distributes hook calls to all registered callbacks.

Minimal example – a plugin in under 30 lines

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}")

Activate with an entry in config/config.json:

{ "sys_active_plugins": ["my_plugin"] }

Done. The next time you start, your menu item will be in the plugins menu, your file_opened handler will respond to every file you open.

Plugin types

Three basic types about PluginType:

Type For what Examples from the Core
EDITOR_PLUGIN Expand Editor: Menus, Functions, Hook Reactions transform_script_plugin, network_discovery_plugin
GUI_PLUGIN Custom tabs, dialogs, windows oscal_plugin, browser_plugin, database_plugin
TOOL_PLUGIN Background tools without their own UI mje_mcp_server_plugin, gui_auto_test_plugin

What exactly can be built?

The included plugins show the range - each of them is a realistic model for your own extensions:

  • Dedicated editor tabs for domain-specific file formats (analogous to the OSCAL plugin with 8 specialized editors).
  • Dock external tools – a plugin can start its own servers (see mje_mcp_server_plugin, which registers a complete MCP server with 154 tools).
  • Database Workbenches as a tab (see database_plugin).
  • Network and inventory tools that write their results directly to open OSCAL documents (see network_discovery_plugin).
  • Transformations and auto-repair for JSON structures (see transform_script_plugin).
  • Web browser or external viewers as an integrated tab (see browser_plugin).
  • Testing and automation plugins that script GUI actions (see gui_auto_test_plugin).
  • File-type reactors that listen on file_opened / file_saved / file_renamed and e.g. B. Trigger validation, conversion or external logging.

Hook reference (excerpt)

Hook Signature Purpose
add_menu (menubar, main_gui) Expand menu bar
add_toolbar (toolbar, main_gui) Expand toolbar
file_opened FileOpenedEvent react to open files
file_saved (file_path, main_gui) react after saving
file_renamed (old_path, new_path) Process renames
file_save_requested (file_path) Save yourself (return True)
save_active_plugin_tab (tab_index) Save active plugin tab for Ctrl+S
get_plugin_file_path (tab_index) Provide file path of a plugin tab
open_external_url (url) Handle external URL plugin internally
on_tab_changed (tab_index, tab_name) respond to tab changes

Domain-specific hooks (e.g. add_excel_resource_to_oscal, update_oscal_resource_base64) are available via the OSCAL plugin and are only active there - your plugins can hook in specifically.

Benefits for developers

  • Quick to first success: First executable plugin in half an hour - example_plugin is in the repository as a copyable starting point.
  • Real API, not a facade: They use exactly the same hooks and helpers that the core team uses to build tabs, menus and tools themselves.- 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 licensed 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 distributing 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 in principle possible and negotiable, but it must be examined in depth how the new license can be designed and what effect it has on the overall functionality of mjEdit. Please contact us using the contact form.

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- Strictly separate GUI from load – Hook registration in on_load(), GUI creation only in on_gui_ready().

  • Do not duplicate global shortcuts – Ctrl+S, Ctrl+W etc. are handled centrally; use save_active_plugin_tab instead of your own shortcuts.
  • Isolate errors – Write hook handler defensively; user-relevant errors via self.show_error(...).
  • use i18n – route visible texts via utils.i18n._().
  • Lazy Imports – only import heavy GUI dependencies on the first call.
  • Own documentation in the plugin – Maintain doc_dev/ and doc_user/ directly in the plugin directory.

Do you want to develop a plugin?

We support plugin authors with API advice, code reviews, AGPL compliance testing and – if necessary – commercial dual licensing. Write to us.