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.pyas Enum + Dataclass events. Calls likefile_openedare delivered via a typedFileOpenedEvent- old signatures remain backwards compatible. - Lifecycle separation: Early
on_load()for registrations, separateon_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_renamedand 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_pluginis 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:
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 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
- 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 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.
- 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 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
- 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- 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_tabinstead 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/anddoc_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.