mjEdit is not just an OSCAL editor — it is a platform. The entire application is built on a documented plugin system: even core features such as the OSCAL editor, network discovery and the MCP server ship as plugins and use exactly the API that is also open to you.

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

What makes it special

  • Open by design: mjEdit is conceived as an extensible application. Features such as the OSCAL tabs, browser tab, database tab, network discovery, MCP server and JSON transform tools are individual plugins — no closed-off internals.
  • Stable hook contracts: Interfaces are versioned in plugins/hook_contracts.py as an enum + dataclass events. Calls like file_opened are delivered through a typed FileOpenedEvent; old signatures stay backward compatible.
  • Lifecycle separation: Early on_load() for registrations, separate on_gui_ready() once the GUI is fully up. This avoids the typical “MainGUI not yet there” crashes of other plugin systems.
  • Robust isolation: Errors inside one plugin hook neither block the core nor other plugins. On unload, menu items, toolbar buttons, editor functions and hooks are cleaned up automatically by BasePlugin.on_unload().
  • Configuration over click-installs: Activation through config/config.json → sys_active_plugins. Version-stable, deployable, Git-friendly.

How it works

plugins/
├── __init__.py          # PluginManager: load, activate, hook dispatch, unload
├── base.py              # BasePlugin: lifecycle + menu/toolbar helpers + cleanup
├── hook_contracts.py    # HookName enum + typed events
└── my_plugin/
    ├── __init__.py      # exports Plugin
    └── plugin.py        # your plugin class

Each plugin exports a class Plugin that inherits from BasePlugin. The PluginManager only loads plugins listed in sys_active_plugins, runs the lifecycle in the right order and dispatches 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 = "My Plugin"
    version = "1.0.0"
    description = "Example for the mjEdit plugin API"
    author = "Your Name"

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

    def on_load(self):
        self.add_menu_item(_("My menu item"), 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(_("My plugin is ready"), timeout=3000)

    def show_message(self, main_gui):
        self.show_info(_("My plugin was triggered."))

    def on_file_opened(self, file_path, content, is_large_file=False):
        self.log(f"File opened: {file_path}")

Activate with one entry in config/config.json:

{ "sys_active_plugins": ["my_plugin"] }

That’s it. On the next start your menu item appears in the Plugins menu and your file_opened handler reacts to every opened file.

Plugin types

Three core types via PluginType:

Type Purpose Core examples
EDITOR_PLUGIN Extend the 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 tooling without own UI mje_mcp_server_plugin, gui_auto_test_plugin

What can you actually build?

The plugins shipped with mjEdit cover the full range — every one of them is a realistic blueprint for your own work:

  • Custom editor tabs for domain-specific file formats (analogous to the OSCAL plugin with its 8 specialised editors).
  • Dock external tools — a plugin can spawn its own servers (see mje_mcp_server_plugin, which registers a full MCP server with 88 tools).
  • Database workbenches as a tab (see database_plugin).
  • Network and inventory tools that write their results directly into open OSCAL documents (see network_discovery_plugin).
  • Transformations and auto-repair for JSON structures (see transform_script_plugin).
  • Web browsers or external viewers as an integrated tab (see browser_plugin).
  • Test and automation plugins that script GUI actions (see gui_auto_test_plugin).
  • File-type reactors listening on file_opened / file_saved / file_renamed for validation, conversion or external logging.

Hook reference (excerpt)

Hook Signature Purpose
add_menu (menubar, main_gui) Extend the menu bar
add_toolbar (toolbar, main_gui) Extend the toolbar
file_opened FileOpenedEvent React to opened files
file_saved (file_path, main_gui) React after saving
file_renamed (old_path, new_path) Handle renames
file_save_requested (file_path) Take over saving (return True)
save_active_plugin_tab (tab_index) Save the active plugin tab on Ctrl+S
get_plugin_file_path (tab_index) Provide a plugin tab’s file path
open_external_url (url) Handle an external URL inside the plugin
on_tab_changed (tab_index, tab_name) React to tab switches

Domain-specific hooks (e.g. add_excel_resource_to_oscal, update_oscal_resource_base64) are exposed by the OSCAL plugin and only fire there — your plugins can hook into them deliberately.

Benefits for developers

  • Fast first success: First running plugin in half an hour — example_plugin ships as a copy-paste starting point.
  • Real API, no façade: You use exactly the same hooks and helpers the core team uses to build tabs, menus and tools.
  • PySide6 + Python: Full Qt power, familiar Python stack, no proprietary DSL.
  • Clean separation: BasePlugin provides safe cleanup, i18n via utils.i18n._(), unified logging and error dialogs without boilerplate.
  • Stable contracts: A HookName enum + dataclass events mean: core refactorings will not silently break your plugin.
  • Great examples: Eight bundled plugins cover almost every extension scenario — from tab GUI to background server.
  • Documentation and tests: doc_dev/PLUGINS_DEV.md is the official, maintained reference. Plugins are testable like normal Python packages.
  • No marketplace gatekeeping: You ship your plugin as a directory — no store, no signature, no approval pipeline.

Licensing — 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. Because your plugin builds directly on BasePlugin, the hook contracts and the internal API of mjEdit (from plugins.base import BasePlugin), it is a derivative work in copyright terms. The copyleft clause of the AGPL therefore 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 the plugin available to third parties or operate it as a service.
  3. Source code disclosure is mandatory — both when distributing binaries (classic GPL obligation) and when providing the software over a network (the AGPL specialty over GPL). Anyone offering mjEdit + your plugin as a service must make the source code of all parts available.
  4. In-house use is unproblematic. As long as the plugin is used only internally and is not redistributed or offered as a network service, no publication obligations arise.
  5. Commercial use is allowed. AGPL ≠ “non-commercial”. You may sell plugins, offer support, build a consulting business around your plugin — you only have to deliver the source code or make it available.
  6. Headers and license text. Adopt the AGPL header that all core files carry (see plugins/base.py) and ship a LICENSE (or COPYING) file.

Practical consequences

Scenario Consequence
Plugin used only inside your company No publication obligation — AGPL requires nothing.
Plugin shipped to customers Plugin source code must be delivered as AGPL-3.0.
mjEdit + plugin as SaaS / web service Source code of all parts must be available to users of the service (AGPL network clause).
Plugin published on GitHub / GitLab AGPL-3.0 license file + header in every source file.
Sell a closed-source plugin Not possible without a separate commercial license from the mjEdit copyright holder.

When you really need closed source

If for business reasons you need a plugin that cannot be released under AGPL — for example because it contains proprietary algorithms or customer data schemas — a commercial dual license for mjEdit is generally negotiable. Please get in touch via the contact form.

Recommendation

For most plugin developers AGPL is an asset, not an obstacle: your plugin benefits from a stable, openly maintained editor core; users gain trust through source openness; auditors and public bodies explicitly prefer AGPL software in compliance contexts.

Getting started

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

Best practices

  • Strictly separate GUI from load — register hooks in on_load(), build GUI 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 handlers defensively; surface user-relevant errors via self.show_error(...).
  • Use i18n — route visible text through utils.i18n._().
  • Lazy imports — defer heavy GUI dependencies until first use.
  • Per-plugin docs — keep doc_dev/ and doc_user/ inside the plugin directory.

Want to build a plugin?

We support plugin authors with API guidance, code reviews, AGPL compliance checks and — when needed — a commercial dual license. Get in touch.