Writing plugins
Plugins extend Mockarty with new content, logic and interface — a pack of ready
mocks, a custom $.fake.* generator, a panel in the sidebar. This guide takes
you from an empty folder to an installed, working plugin in a few minutes.
If you only want to install a plugin someone else made, see
Plugins. This page is for authors.
Quickstart — create, package, and enable a plugin
For a mock kit, the CLI creates the starter files and packages them. You only
need a running Mockarty instance and an administrator token to install and
enable the bundle.
mockarty-cli plugin create my-plugin # scaffold (add --template for others)
mockarty-cli plugin pack my-plugin # → my-plugin-0.1.0.zip
mockarty-cli plugin install my-plugin-0.1.0.zip && mockarty-cli plugin enable my-plugin
The example enables a starter mock kit. Edit its content before sharing it.
For a WASM faker, also run ./build.sh (needs TinyGo).
While iterating, mockarty-cli plugin dev my-plugin --enable re-installs on every
save. Use --template with plugin create to start from another plugin type.
What a plugin is
A plugin is a small .zip bundle with one file that matters — plugin.json, the
manifest — plus any assets it ships (a WebAssembly module, an HTML panel).
The manifest declares what the plugin contributes; Mockarty renders and applies
those contributions itself. The mechanisms:
| Mechanism | You write | Runs where |
|---|---|---|
| Mock kit | JSON only | — (declarative content) |
| WASM faker | a tiny function, compiled to WebAssembly | in a locked sandbox |
| WASM response transformer | a tiny function, compiled to WebAssembly | in a locked sandbox, inline on the response |
| WASM request matcher | a tiny function, compiled to WebAssembly | in a locked sandbox, on the match path |
| Protocol codec | a line-frame encoder/decoder compiled to WebAssembly | in a locked sandbox behind the host-owned listener |
| UI panel (sidebar / page-slot / command / entity-tab / settings) | an HTML page | in a sandboxed iframe |
| Wiki macro | JSON + a text/template | expands on wiki pages, listed in the editor slash-menu |
| Connector | JSON only | reuses a built-in integration adapter |
| Content pack | JSON only | seeds another module (wiki / dashboards / issues / collections / test cases / contracts) on demand |
Plus optional settings (a JSON-Schema config form) on any of them.
You can upload the bundle from a file without a registry connection.
The authoring loop
Everything below uses mockarty-cli. Scaffold, package and dev-reload work fully
offline; only install/enable need a running server and an admin token.
mockarty-cli plugin create my-plugin # scaffold (mock-kit template)
mockarty-cli plugin pack my-plugin # → my-plugin-0.1.0.zip
mockarty-cli plugin install my-plugin-0.1.0.zip # upload to a server (admin)
mockarty-cli plugin enable my-plugin # turn it on
Pick a template
create scaffolds a package that already validates and passes conformance, so
the first thing you do is edit content, not fight a schema:
--template |
Scaffolds |
|---|---|
mock-kit (default) |
a pack of ready mocks |
wasm-faker |
a $.fake.* generator + a build.sh (needs TinyGo) |
protocol-codec |
a client-first line protocol + WASM codec + build.sh |
ui-panel |
a sidebar panel — an HTML page in a sandboxed iframe |
connector |
a connection type that reuses a built-in integration adapter |
content-pack |
seed content for another module (wiki, dashboards, issues, …) |
link-type |
a link type between entities, with a deep-link URL template |
event-type |
an event other modules can subscribe to |
event-listener |
a subscription to host events, plus an event of your own |
task-type |
a task type a runner can execute |
wiki-macro |
a macro that expands on wiki pages |
agent |
a specialist agent persona the agent network can route work to |
skill |
a reusable agent recipe the host hands to an agent on demand |
mcp-tool |
your runner task exposed to agents as a first-class MCP tool |
workflow-component |
your runner task, selectable as a mission step |
aqc-surface |
a custom quality surface plus the processor that fills it |
Every contribution family the server understands has a template here, with one
exception: a worker ships real executables, and its manifest pins each
artifact’s sha256 — a scaffold cannot invent a binary. Start a worker from the
task-type template and add the workers block by hand.
Check it before anyone else sees it
plugin test runs everything the server would check at install time, plus the
things a successful install still leaves wrong — a contribution with no title
nobody can find in a picker, a permission the manifest asks for but nothing
uses, a settings field named api_token that would hold the token in plaintext.
It needs no server and no network:
mockarty-cli plugin test ./my-plugin # a directory or a packed .zip
mockarty-cli plugin test ./my-plugin --strict # fail on warnings too (use this in CI)
mockarty-cli plugin test ./my-plugin --report r.json # portable report for a reviewer
mockarty-cli plugin test ./my-plugin --report r.json --sign-key key.b64 # signed
Every finding carries its fix on the next line. Errors fail the command, so a CI
job that runs this cannot publish a package the host would refuse.
Set MOCKARTY_CONFORMANCE_HOST_VERSION to also check the manifest’s declared
version range against a specific host; without it that check is skipped rather
than guessed.
Editor completion and CI validation
plugin schema prints the manifest JSON Schema — generated from the same
vocabularies the server enforces, so it cannot drift from them:
mockarty-cli plugin schema --out plugin.schema.json
Point your editor at it ("$schema": "./plugin.schema.json" in plugin.json)
and you get completion and inline errors while you type.
While iterating, skip the manual pack/install cycle — let dev watch and reload:
mockarty-cli plugin dev my-plugin --enable # re-installs on every save
Validate a bundle any time, no server needed:
mockarty-cli plugin inspect my-plugin-0.1.0.zip
The manifest
{
"id": "acme.ru-fakers",
"name": "Russian fakers",
"version": "1.0.0",
"description": "What this plugin adds.",
"author": { "name": "Acme", "url": "https://acme.example" },
"license": "MIT",
"min_mockarty_version": "2.0.0",
"contributes": { }
}
- id — a lowercase, dot-namespaceable slug (
acme.ru-fakers). It is the
plugin’s permanent identity; pick it once. - version — semver. Installing a higher version over a lower one is an
in-place upgrade that keeps the enabled state. - min_mockarty_version — optional; refuse to install on older servers.
- contributes — the payload. One or more of the sections below.
Validation is strict and the error messages name the exact field, so a typo
fails at pack/install, never silently.
Mock kit (no code)
The simplest plugin: a ready-made contour of mocks. Once enabled, the kit shows
up in the mock-kits catalogue, ready to instantiate into any namespace.
{
"contributes": {
"mock_kits": [
{
"key": "demo_users_api",
"name": "Users API (demo)",
"description": "A starter /users contour.",
"mocks": [
{ "route": "/users", "method": "GET", "status_code": 200,
"body": { "users": [ { "id": "$.fake.UUID", "name": "$.fake.Name" } ] } },
{ "route": "/users", "method": "POST", "status_code": 201,
"body": { "id": "$.fake.UUID", "created": true } }
]
}
]
}
}
Bodies support the same dynamic helpers as any mock ($.fake.*, JSONPath).
Scaffold one with mockarty-cli plugin create my-kit --template mock-kit.
A kit mock can do everything a hand-made mock can
A kit is only worth shipping if the contour it stands up is complete — otherwise
whoever instantiates it has to hand-build the interesting half. So a kit mock
takes the same fields a mock takes, with the same names:
| Field | What it gives you |
|---|---|
conditions |
Several mocks on one route, told apart by the request body. This is how a kit ships a normal reply, an error and a streamed reply on the same endpoint. |
header_conditions, query_conditions |
The same, matched on headers or query parameters. Query conditions are HTTP-only — a tool call carries arguments, not a query string. |
headers |
Response headers — Retry-After on a 429, a custom trace id. Declaring any header means you own them all; the application/json default is not re-added. |
delay |
Milliseconds before the response (up to 120000). A slow upstream; works for HTTP and for MCP tools. |
priority |
Chooses between mocks whose conditions matched: the higher value wins. A mock with matching conditions still outranks a catch-all mock. |
llm |
A provider-shaped chat reply — see below. |
sse |
An explicit server-sent-event chain, for non-LLM streams. The mock is served to event-stream clients on its own route. |
protocol + mcp |
Serve the mock as an MCP tool instead of a URL. |
Among mocks whose conditions matched, the highest priority wins; specificity
breaks ties at the same priority. A mock without conditions is the catch-all and
is used only when no conditioned mock matched. Write the general case without
conditions, each special case with conditions, and use priority to order
overlapping special cases explicitly.
{
"mocks": [
{ "route": "/v1/chat/completions", "method": "POST", "status_code": 429,
"priority": 90,
"conditions": [ { "path": "$.model", "assertAction": "equals", "value": "mock/rate-limit" } ],
"headers": { "Content-Type": ["application/json"], "Retry-After": ["2"] },
"body": { "error": { "message": "Rate limit reached.", "code": "rate_limit_exceeded" } } },
{ "route": "/v1/chat/completions", "method": "POST", "status_code": 200,
"llm": { "provider": "openai", "model": "gpt-4o-mini",
"content": "You said: $.req.lastUserMessage",
"finishReason": "stop", "chunkChars": 6, "chunkDelayMs": 25 } }
]
}
LLM replies (llm)
provider picks the wire format — openai (also correct for DeepSeek, Qwen,
Mistral, Ollama and other OpenAI-compatible APIs) or anthropic. Streaming
needs no second mock: when the request sets "stream": true the same mock is
streamed token by token, with chunkChars and chunkDelayMs controlling the
pace. toolCalls returns a function call instead of text, and finishReason
must use the provider’s own vocabulary (stop/tool_calls/length for OpenAI,
end_turn/tool_use/max_tokens for Anthropic) — clients switch on the exact
string.
MCP tools (protocol: "mcp")
Set protocol to "mcp" and the mock answers a tool call rather than a URL.
mcp.tool is the name a client sees, mcp.description is what a calling model
reads to decide whether to use it, and mcp.input_schema is the JSON Schema
of its arguments — declare it. Without it the server can only infer a schema
from your conditions, and that inferred schema has no required and flattens
nested arguments, which is exactly what a model needs to call the tool
correctly. Set mcp_is_error: true on a variant to return a tool error.
{
"protocol": "mcp",
"mcp": {
"tool": "browser_navigate",
"description": "Navigate the browser to a URL and return the page title.",
"input_schema": {
"type": "object",
"properties": { "url": { "type": "string", "description": "Absolute URL to open" } },
"required": ["url"]
}
},
"body": { "content": [ { "type": "text", "text": "Navigated to $.req.url" } ] }
}
Clients reach the mocked MCP server at POST /stubs/<namespace> and discover the
tools with tools/list. Ready-made examples of both kinds live in
examples/plugins/ — llm-openai, llm-anthropic and mcp-playwright,
mcp-github and friends.
WASM faker (custom $.fake.*)
A faker plugin adds new dynamic-response generators — for example valid national
identifiers. The logic runs as a WebAssembly module in a locked sandbox: it
can read and write only its own memory — no disk, no network, no clock. That is
the whole security story: a code plugin cannot touch anything without being
granted it.
Scaffold one:
mockarty-cli plugin create my-fakers --template wasm-faker
You get a manifest, a TinyGo source and a build script:
{
"contributes": {
"wasm": [
{ "point": "faker-provider", "module": "faker.wasm",
"fn": "faker", "exports": ["my_id"] }
]
}
}
- module — the compiled
.wasmasset in the bundle. - fn — the exported function the host calls.
- exports — the faker names this function serves. Listing
my_idmakes
$.fake.my_idavailable in any mock body.
The guest contract
Mockarty passes the requested faker name in and expects a value back, as small
JSON strings. Your module exports three things:
memory your linear memory
mk_alloc(size i32) -> i32 a bump allocator returning a pointer
<fn>(ptr i32, len i32) -> i64 packs (outPtr << 32 | outLen) of the result
For the faker point the input is {"name":"<faker>"} and the output is
{"value":"<string>"}. One function serves every name in exports; branch on
the name field. The scaffold implements all of this — you edit only the value
you return.
Build it
The product needs no toolchain to run a plugin — it carries its own WebAssembly
runtime. You need TinyGo only to compile the module:
cd my-fakers && ./build.sh # tinygo build -target=wasm-unknown -o faker.wasm .
mockarty-cli plugin pack .
A worked example with valid check digits (ИНН/СНИЛС/ОГРН/КПП) ships in the
product’s examples/plugins/ru-fakers — a good starting point for real
generators.
Randomness: a sandboxed module has no entropy source, so seed a small PRNG
that advances per call. Values then vary call-to-call (never cached) but are
deterministic per server start — ideal for reproducible test data.
WASM response transformer (rewrite a mock’s body)
A response-transformer plugin post-processes a mock’s rendered body just before
it is sent — masking a field, wrapping an envelope, redacting a token. It runs as
a WebAssembly module in the same locked sandbox as a faker, inline on the
response path, and only for the mocks you opt in by tag.
{
"contributes": {
"wasm": [
{ "point": "response-transformer", "module": "transform.wasm",
"fn": "transform", "exports": ["envelope"] }
]
}
}
- point —
response-transformer. - fn — the exported function the host calls with the body bytes.
- exports — the transformer keys this function serves. Listing
envelope
lets a mock opt in with the tagtransform:envelope.
Opt in per mock. Add a tag transform:<key> to any mock. Its rendered body
is piped through the transformer bound at <key>; untagged mocks pay nothing.
Several transform: tags chain in order.
The guest contract
Same three exports as a faker, but the payload is the raw body, not JSON:
memory your linear memory
mk_alloc(size i32) -> i32 a bump allocator returning a pointer
transform(ptr i32, len i32) -> i64 packs (outPtr << 32 | outLen) of the new body
transform receives the body bytes at (ptr,len) and returns the transformed
body. The host caps the input it will hand you (1 MiB by default) — a larger body
is served unchanged rather than truncated, so size your input arena for that cap.
A transformer that errors or traps is skipped and the original body is sent — it
can never break a response.
A worked example that wraps any body as {"data":<body>,"transformedBy":"plugin"}
ships in examples/plugins/response-transformer.
WASM request matcher (custom condition operator)
A matcher plugin adds a new condition operator you can use anywhere a mock
matches a request — a check the built-in operators (equals, contains, matches,
gt/lt, one_of, …) don’t cover, like a Luhn checksum or a national-ID check digit.
It runs as a WebAssembly module in the same locked sandbox, and only for the
conditions that name it — every built-in operator is decided without ever calling
your module.
{
"contributes": {
"wasm": [
{ "point": "condition-matcher", "module": "match.wasm",
"fn": "match", "exports": ["luhn", "divisible_by"] }
]
}
}
- point —
condition-matcher. - exports — the operator keys this function serves. Listing
luhnlets a
condition useassertAction: "plugin:luhn".
Use it on a mock. Set a condition’s assertAction to plugin:<key>. Its
path selects the value to check; its value is the operand (for operators that
take one, like divisible_by). A condition whose matcher isn’t installed — or is
disabled for the namespace — simply never matches (the safe default).
The guest contract
Same three exports as a faker. The host passes the invoked key plus the value and
operand as JSON, and expects a boolean:
memory your linear memory
mk_alloc(size i32) -> i32 a bump allocator returning a pointer
match(ptr i32, len i32) -> i64 packs (outPtr << 32 | outLen) of the result
Input is {"key":"<key>","actual":"<value>","expected":"<operand>"}; output is
{"match":true|false}. One function serves every key in exports; branch on the
key field. A matcher that errors or traps counts as no-match — it can never
crash request matching.
A worked example (plugin:luhn, plugin:divisible_by) ships in
examples/plugins/condition-matcher.
Protocol codec (client-first TCP lines)
A protocol plugin adds a safe codec, not a network server. Mockarty owns the
listener, namespace routing, 1 MiB frame limit, Socket-mock resolution, request
logs, undefined-request capture and connection shutdown. The guest only maps a
newline-delimited frame to a JSON message and maps the selected mock response
back to one frame.
mockarty-cli plugin create my-codec --template protocol-codec
./my-codec/build.sh # needs TinyGo
mockarty-cli plugin test ./my-codec
The manifest binds one descriptor to exactly one WASM export with the same key:
{
"contributes": {
"protocols": [{
"key": "acme_line", "name": "ACME line", "transport": "tcp-line",
"magic": "ACME "
}],
"wasm": [{
"point": "protocol-codec", "module": "codec.wasm",
"fn": "codec", "exports": ["acme_line"]
}]
}
}
The host calls codec with
{"operation":"decode|encode","protocol":"acme_line","frameBase64":"..."}.
For decode, return {"message":{...}}; for encode, return
{"frameBase64":"..."}. The host overwrites serverName and
pluginProtocol, so guest code cannot route into another protocol or erase
attribution. Calls have a 100 ms deadline and no disk, network, environment or
clock access.
Start Mockarty with MOCKARTY_UNIFIED_PORT=1, enable the plugin, then run
mockarty-cli plugin protocols. Create a Socket mock with the returned
serverName and the event emitted by decode; the Constructor’s Socket section
can fill that routing value from the active-plugin picker. Every resolved call
uses the ordinary request log, while unmatched decoded messages enter Undefined
Requests and can be turned into a mock.
This first transport is deliberately bounded: client-first, printable ASCII
magic (2–16 bytes), UTF-8-safe JSON message, newline framing, one request/reply
stream. Binary framing, server-first handshakes and arbitrary socket state need
a new host transport; they cannot be smuggled into guest WASM.
UI panel (a page in the sidebar)
A UI plugin adds a sidebar item that opens your own HTML page. The page renders
in a sandboxed iframe served from your bundle: strict CSP, no same-origin,
so it can never read the host session or page. It talks to Mockarty only through
postMessage.
mockarty-cli plugin create my-panel --template ui-panel
{
"contributes": {
"ui": [
{ "point": "sidebar", "id": "my_panel", "title": "My panel",
"icon": "wrench-screwdriver", "panel": "panel.html" }
]
}
}
- icon — a heroicon name (without the
hi-prefix). Pick one that exists or
the slot renders blank. - panel — a bundle-relative HTML file.
Mockarty appends the current theme as ?theme=dark|light so your panel can match
light and dark. The scaffold panel.html is theme-aware and self-contained —
edit it into your interface. It follows theme toggles live.
Embedding a panel inline (page-slot)
Besides the sidebar, a ui contribution can render its panel inline on a host
page by using point: "page-slot" with a target naming a host slot:
{ "point": "page-slot", "id": "ops_slot", "title": "Ops", "target": "dashboard", "panel": "panel.html" }
The host exposes UI points beyond the sidebar, all using the same sandboxed
iframe: page-slot (a panel in a page region — target is the page id;
live on 12 screens incl. dashboard, mocks, tasks, test-cases);
command (a Ctrl-K palette action that opens your panel, reachable from any
screen); entity-tab (a tab in an entity card — target is the entity type:
issue renders next to Comments/History, mock next to Config/Logs/Versions/Info,
case inside the test-case builder next to Steps…History; plugin tabs carry a
small puzzle mark so they’re distinguishable from built-in tabs); and
settings-section (a sandboxed companion panel inside the plugin’s
Configure modal, below the host-generated JSON-Schema form). A plugin can
contribute several points from one panel.html (see
examples/plugins/ops-panel).
Adding a diagram format to boards (board-import)
A board-import contribution puts your format into the board import picker,
next to Mermaid, draw.io and board scenes, with a Plugin badge. target is
the format slug:
{ "point": "board-import", "id": "plantuml", "title": "PlantUML",
"target": "plantuml", "icon": "document-text", "panel": "import.html" }
When the user picks your entry, the host loads panel.html in the usual
sandbox — invisibly, as a converter — and sends it the user’s input:
window.addEventListener('message', (e) => {
if (e.data && e.data.type === 'mockarty:board-import-request') {
const mermaid = convert(e.data.text); // your conversion
parent.postMessage({ type: 'mockarty:board-import',
format: 'mermaid', payload: mermaid }, '*');
}
});
Answer with format set to mermaid, drawio or scene and payload as the
text in that format; the host converts it into shapes itself. To report a
problem, answer { type: 'mockarty:board-import', error: 'why' } — the user
sees the reason. A panel that does not answer within a minute is dropped.
External panel (panel_url) — bring your own service
Instead of a bundled panel, a ui contribution can point at a page your own
service hosts — set panel_url to an absolute https URL (one of panel /
panel_url is required, never both):
{ "point": "sidebar", "id": "status_board", "title": "Status board",
"panel_url": "https://plugin.example.com/board" }
The page loads in the same sandboxed iframe as a bundled panel — it gets the
?theme= hint but no host session and no same-origin access. Use this when the
panel is a living product with its own backend (dashboards, SaaS consoles):
nothing to re-bundle on every release, the URL always serves your latest UI.
Users see the panel’s origin in the manifest before installing.
Wiki macro (Confluence-style, no code)
A wiki-macro plugin adds a new !plugin-<name>:… block to every wiki page and
to the editor’s / slash-menu — the Confluence macro gesture, declaratively:
"contributes": { "wiki_macros": [
{ "name": "plugin-status", "title": "Status lozenge", "icon": "tag",
"params": [ { "name": "colour", "title": "Colour", "default": "grey" } ],
"template": "<span data-macro=\"plugin-status\" data-macro-args=\"{{.colour}}\">{{.text}}</span>" } ] }
Usage on a page: !plugin-status:colour=green SHIPPED. Arguments are
key=value pairs (quote values with spaces: key="a b"); leftover words
arrive as {{.text}}. The plugin- name prefix is mandatory, so a plugin
macro can never shadow a built-in.
- template is a Go
text/template. Its output re-enters the normal
markdown pipeline — the Markdown renderer, then the sanitizer — so a macro can produce
nothing an author couldn’t type by hand; scripts and inline styles are
stripped. Output is capped at 64 KiB. A template that does not parse fails
at INSTALL, never at page view. - The pinned attribute pair
data-macro/data-macro-args(ondiv/span)
is allowed through the sanitizer so the host can style macro output — status
lozenges get colours out of the box (grey/green/red/yellow/blue by the first
args token). - Per-namespace activation applies: where the plugin is disabled, the macro
line stays as authored text — visible degradation, never silent loss. - Enabled macros appear automatically in the editor’s slash-menu with a 🧩
marker.
Worked example: examples/plugins/wiki-status-macros (a status lozenge and a
DECISION panel).
Connector (a plugin for an external tool)
A connector plugin extends Mockarty toward an external tool — Jira, GitHub,
GitLab, TestRail — with no code. It ships a pre-configured binding onto an
integration adapter Mockarty already carries, so installing it gives you a
ready-to-use connector with sensible defaults.
mockarty-cli plugin create my-jira --template connector
{
"contributes": {
"connectors": [
{ "key": "jira_cloud", "name": "Jira Cloud", "kind": "jira",
"icon": "bug-ant",
"config_template": { "base_url": "https://your-org.atlassian.net", "project_key": "QA" } }
]
}
}
- kind — a built-in adapter:
jira,github,gitlab,testrail, and
more. It is validated at install; an unknown kind is refused, because a plugin
reuses a ready adapter rather than shipping one. - config_template — defaults that pre-fill the integration when a user
creates it from this connector. Never put secrets here — credentials are
entered at setup.
Enabled connectors are listed at GET /api/v1/plugin-connectors and in the
Plugins UI; creating an integration from one uses its kind and
config_template as defaults, and the actual calls run through the built-in
adapter. This is the pattern for any tracker/CI/chat: reuse a ready adapter,
curate a connector, distribute it as a plugin. The examples/plugins/jira-connector
example is a complete, working starting point.
Settings (a configuration form)
A plugin can declare a settings schema — a JSON Schema of its configurable
options. When present, the admin Plugins page shows a Configure form
generated from the schema, and saved values are validated against it before they
persist.
{
"id": "acme.ops",
"name": "Ops",
"version": "1.0.0",
"settings_schema": {
"type": "object",
"properties": {
"heading": { "type": "string", "title": "Panel heading" },
"refresh_seconds": { "type": "integer", "minimum": 1, "maximum": 60 },
"show_clock": { "type": "boolean", "title": "Show the live clock" }
}
},
"contributes": { "ui": [
{ "point": "sidebar", "id": "ops", "title": "Ops", "panel": "panel.html" },
{ "point": "settings-section", "id": "ops_settings", "title": "Ops preview",
"icon": "wrench-screwdriver", "panel": "panel.html" }
] }
}
The form supports string, number/integer (with minimum/maximum), boolean and
enum (dropdown) properties, plus title and description. The admin form
saves the plugin’s instance default; a namespace override, when present, wins
for that namespace. Both survive plugin upgrades.
A settings-section requires settings_schema and exactly one of a bundled
panel or an HTTPS panel_url; it has no target. It is mounted only while the
plugin is active in the current namespace, carries a visible Plugin badge,
and runs in the same strict sandbox as every other plugin panel. The section is
a companion interface — the host-generated form remains the only place that
saves values.
Never store secrets in settings. Settings are readable by any user who can
open the plugin (the host hands them to the panel), so treat them as public
configuration — URLs, labels, toggles. Credentials belong in a secret store,
not here.
Reading settings in a panel. A sandboxed panel can’t fetch its own settings
(it has no authenticated origin), so the host reads them and pushes them in via
postMessage when the panel loads. A bundled panel receives the values effective
for the current namespace. An external panel_url must declare the
ui:external-panel permission; without that reviewed permission, settings are
not handed to the external origin. Opt in by listening for the message:
window.addEventListener('message', function (ev) {
if (ev.source !== window.parent) return; // only trust the host
if (!ev.data || ev.data.type !== 'mockarty:settings') return;
var s = ev.data.settings || {};
if (s.heading) document.querySelector('h1').textContent = s.heading;
});
The examples/plugins/ops-panel example does exactly this.
Signing (optional, for regulated environments)
A server can require plugins to be signed by a key its operators trust. Generate
a key pair, sign a bundle, and hand the public key to the server operator:
mockarty-cli plugin keygen --out mykey # mykey (private), mykey.pub
mockarty-cli plugin pack my-plugin --sign mykey # signed bundle
The operator configures which keys to trust (and whether signatures are
mandatory). An unsigned or untrusted bundle is then refused on such servers.
mockarty-cli plugin inspect reports a bundle’s signature status.
Content pack — seed another module (no code)
A content pack ships ready-made content for a module beyond mocking and lets a
user instantiate it on demand into a namespace. Today the wiki module is wired
(a Confluence-style starter: runbooks, ADRs, PRDs); dashboards and collections
follow the same shape. Nothing is created until someone instantiates the pack, so
enabling the plugin never touches a namespace’s data.
"contributes": { "content_packs": [
{ "key": "team_wiki", "name": "Team wiki starter", "kind": "wiki",
"description": "Runbook, ADR and PRD pages.",
"items": [
{"title": "Incident runbook", "content": "# Incident runbook\n\n## Summary\n…",
"children": [{"title": "Postmortem", "content": "# Postmortem\n…"}]},
{"title": "ADR", "content": "# ADR-000: <title>\n\n## Context\n## Decision\n"}
] } ] }
- kind — which module the pack targets:
wiki(pages, nested viachildren)
ordashboard(a widget dashboard:itemsis[{name, description, widgets:[{widgetType, dataSourceId, x, y, w, h, params?}]}]), orissue(a starter project + issues:itemsis[{name, keyPrefix, description?, issues:[{title, type, description?, priority?}]}], type ∈ task/story/bug/epic), orcollection(API-Tester collections:itemsis[{name, protocol?, requests:[{name, method?, url, body?}]}]), ortcm(test cases with manual steps:itemsis[{name, priority?, expectedResult?, tags?, steps:[{action, expectedResult?}]}]), orcontract(starter API contracts published into the registry:itemsis[{serviceName, specContent, specType?, version?, description?, tags?}]— spec type is auto-detected when omitted), orboard(whiteboard templates:itemsis[{name, description?, tags?, scene?}]wheresceneis plain Excalidraw JSON — seeexamples/plugins/board-templates). Packs that target a licensed module (API Tester, TCM, contract testing, whiteboards) require that feature on the namespace. - items — kind-specific content (for
wiki, a tree of{title, content, children}).
After install + enable, the pack appears in the content-pack catalogue
(GET /api/v1/content-packs, or the content_pack_list MCP tool) for any
namespace where the plugin is active; instantiate it with
POST /api/v1/content-packs/<key>/instantiate (or content_pack_instantiate).
Example: examples/plugins/team-wiki-pack.
Link types — relate entities to external systems (no code)
A link contribution declares a typed relation between a Mockarty entity and
another system — “this issue has a Jira ticket”, “this mock came from a GitHub
issue”. The URL template turns a stored key into a clickable deep link (every
{key} is replaced), so a linked entity is one click from its counterpart.
"contributes": { "links": [
{ "key": "jira_ticket", "name": "Jira ticket", "icon": "bug-ant",
"from_entity": "issue", "to_entity": "external",
"url_template": "https://your-org.atlassian.net/browse/{key}" } ] }
- from_entity / to_entity — one of
mock,issue,case,wiki,
collection,dashboard,contract,scan,external. - url_template — optional absolute http(s) URL;
{key}is the stored value.
Once enabled, the types appear in GET /api/v1/entity-link-types (filter with
?entity=issue) for any namespace where the plugin is active, and entity cards
(e.g. an issue) show an External links section to add/remove stored links
(POST/GET/DELETE /api/v1/entity-links; MCP entity_link_*). Example:
examples/plugins/tracker-links.
Event types — extend the notification catalogue (no code)
An event-type contribution adds new events users can subscribe to per channel
exactly like built-in ones (email, Telegram, Slack, in-app …). They appear in the
notification catalogue under the plugin category.
"contributes": { "event_types": [
{ "type": "plugin.deploy.started", "title": "Deploy started", "severity": "info" },
{ "type": "plugin.deploy.failed", "title": "Deploy failed", "severity": "error",
"description": "A deployment failed and needs attention." } ] }
- type — must be namespaced
plugin.<name>, so it can never shadow a
built-in event. - severity —
info(default),success,warning,errororcritical. - default_subscribed — opt users in by default (rare).
Example: examples/plugins/deploy-events.
Emitting events at runtime
A plugin surface with an API credential — a managed worker, an external
process, a panel through the host page — publishes a declared event with:
curl -X POST "https://your-mockarty/api/v1/plugin-events/<plugin-id>/emit" \
-H "Authorization: Bearer <api-token>" -H "Content-Type: application/json" \
-d '{"eventType":"plugin.deploy.failed","correlationId":"deploy-42",
"payload":{"stage":"migrate"}}'
Only event types the manifest declares are accepted, only while the plugin is
installed and active in the namespace, and the payload must be a JSON object
(up to 256 KiB). The host stamps the envelope version, the plugin id and the
optional correlationId/causationId lineage into the delivered payload —
subscribers receive them under those exact keys.
Task types — dispatchable jobs for external runners (no code)
A task-type contribution names a new runner job kind. Pair it with an
external runner that advertises the matching capability (the standard runner
protocol), and submitted tasks of that type are dispatched to it — how a vendor
ships heavy, out-of-process functionality.
"contributes": { "task_types": [
{ "type": "plugin-dbt-run", "name": "dbt run",
"description": "Execute a dbt model run on an external runner." } ] }
- type — a lowercase slug namespaced
plugin-<name>, max 19 chars, so it can
never shadow a built-in task type. Plugin task types are always licence-free —
reachability is governed by the plugin’s own enable state.
Example: examples/plugins/custom-runner-tasks.
Import from another tool (WireMock / Postman / Mockoon / Bruno / Insomnia / Connect / n8n)
Already have a mock library in another tool? Turn it into a Mockarty plugin with
one command — the shared “route + method + status + JSON body” shape converts
directly into a mock kit:
mockarty-cli plugin import ./mappings --from wiremock --id acme.legacy-stubs
mockarty-cli plugin import ./collection.json --from postman --id acme.api
mockarty-cli plugin import ./env.json --from mockoon --id acme.env
mockarty-cli plugin import ./my-collection --from bruno --id acme.api
mockarty-cli plugin import ./export.json --from insomnia --id acme.api
- WireMock — a single mapping file, a
{"mappings":[…]}file, or a folder of
per-mapping.jsonfiles.jsonBody/bodyandurlPath/urlare handled. - Postman — a v2.1 collection; nested folders are walked and each request’s
first saved example response becomes the mock.{{var}}path variables become
:var. - Mockoon — an environment export; each route’s default (or first) response
is used. - Bruno — a collection folder of
.brufiles (pass the folder). Each
request becomes a 200 mock; abody:jsonblock becomes the mock body, and
{{var}}path segments become:varparams. Bruno stores no response
examples, so responses start as the request body or empty — edit after import. - Insomnia — an export v4 file, JSON or YAML flavour (Application menu →
Export). Requests become 200 mocks (JSON request bodies carried over);
{{ _.base_url }}prefixes are stripped, in-path templates become:params.
The command writes a ready-to-review plugin directory; then pack and install
it like any other. Bodies that are a JSON object pass through unchanged; a
top-level array or scalar is wrapped as {"response": …} (a kit body is an
object).
Atlassian Connect apps (--from connect)
A Connect app is already “descriptor + your hosted service + iframe pages +
webhooks” — the same shape as a Mockarty plugin with external panels. Convert
the descriptor as-is:
mockarty-cli plugin import ./atlassian-connect.json --from connect
generalPages/adminPages/webSectionsbecome sidebar panels,
webPanelsbecome dashboard page-slot panels — all opening your existing
https service viapanel_url(no backend changes;--idis optional, it
derives from the Connectkey).webhooksbecome subscribable plugin event types;scopesare recorded as
informational permissions shown at install time.- Not carried over: the JWT handshake, workflow modules, and
{context.param}
query placeholders (queries are dropped from panel URLs) — the panel runs in
Mockarty’s sandbox, not in Jira’s context protocol. The descriptor’sbaseUrl
must be a real https URL (templated dev values like{{localBaseUrl}}are
refused with a clear error).
Review the generated plugin.json, then pack and install as usual.
n8n community nodes (--from n8n)
n8n’s node library is a huge catalogue of integration definitions. Point the
converter at a folder of nodes (a community node package, or a checkout of
n8n-nodes-base/nodes subfolders) and it emits ONE connector plugin — every
node becomes an integration preset:
mockarty-cli plugin import ./nodes --from n8n
- Nodes matching a built-in Mockarty adapter (
github,gitlab,jira,
jenkins,linear) reuse that adapter; everything else becomes a
webhook_genericpreset, seeded with the node’s declared API base URL when
its source carries one. - The service name, description, category and documentation link carry over;
trigger variants fold into their base node. - Node execution logic and credential flows do NOT carry over — a preset gives
you the service’s identity and API base so the integration is created in two
clicks, not a workflow runtime.
--id is optional (default n8n.connectors). Review, pack, install.
Publishing to a registry
There is no central store to gatekeep you. A registry is just a Git repository
with an index.json listing plugins and pointing at their downloadable zips —
the same model as a Homebrew tap. Prepare your entry:
mockarty-cli plugin publish my-plugin-1.0.0.zip \
--download-url https://github.com/you/registry/releases/download/my-plugin-v1.0.0/my-plugin-1.0.0.zip
Add the printed object to the registry’s index.json, upload the zip as the
release asset, and it becomes installable by id on any server pointed at that
registry. A seed index.json and the example plugins ship in the product’s
examples/plugins/ folder.
Server vs desktop
A plugin behaves identically on a full server and on the desktop build — the
bundle is stored the same way and applied the same way. WASM fakers and UI panels
work in both. On a cluster, enabling a plugin on one node propagates to the others
automatically. Nothing in a plugin is desktop- or server-specific.
Checklist
plugin.jsonvalid (mockarty-cli plugin inspect)- One clear
contributessection per thing you add - WASM:
.wasmbuilt and packed; fakers listed inexports - UI:
iconexists; panel is theme-aware; no assumption of host DOM access - Semver bumped on every change you ship