Manifest reference
What the app accepts from a plugin’s manifest, field by field. This page is derived from the loader’s validator; when the two disagree the validator wins. The authoring walkthrough — project layout, handler code, hooks, settings, inbound triggers — is under Guides.
A manifest that fails validation is refused with one message per broken rule and the plugin is not loaded.
Top level
Section titled “Top level”| Field | Required | Rule |
|---|---|---|
name |
yes | kebab-case (^[a-z0-9]+(-[a-z0-9]+)*$). Doubles as the plugin’s identity in the installed registry and in the pluginAllowList a folder agent may carry. |
displayName |
yes | free text, shown in Connect. |
version |
yes | semver MAJOR.MINOR.PATCH with an optional -prerelease suffix. |
description, author { name, url? }, repository, icon, license, keywords[], category |
no | descriptive only. |
capabilities[] |
no | flat lowercase tags (^[a-z0-9]+([.-][a-z0-9]+)*$), e.g. channel.telegram, audio.stt. A dot-separated namespace is recommended, not enforced. Consumers resolve a capability to the first plugin tool that declares it; absent means []. |
runtime |
no | "node" (default) — handlers are JavaScript files run in the app’s plugin host; "process" — the plugin is its own executable, see below. |
command, args[] |
process only |
command is required for runtime: "process", must be relative to the plugin’s install directory (absolute on either OS is rejected — manifests are portable), and must not contain ... |
engines.filer |
no | a version range the plugin targets. |
bundled |
no | true for plugins shipped with the app. |
contributes |
no | see below. |
sandbox |
no | true runs node handlers in the sandbox worker. Rejected for runtime: "process" (the executable is already its own OS process). |
permissions |
no | see Permissions. |
runtime: "process" — not supported
Section titled “runtime: "process" — not supported”The process runtime speaks the plugin’s tools over a process transport; a tool is called by its own id, so a
tool entry must not carry handler. The following are rejected under this runtime, each with its own message:
contributes.hooks (non-empty), contributes.views (non-empty), any contributes.settings[*] of type: "action",
sandbox, and a static permissions.fs.read/write rule without "parameter": true — a process plugin does its
own OS I/O and never receives the ctx.fs proxy that enforces static rules, so such a rule would be silently
unenforced; use "parameter": true (dispatch-time enforcement) instead.
contributes
Section titled “contributes”tools[] — MCP tools the agent can call
Section titled “tools[] — MCP tools the agent can call”| Field | Required | Rule |
|---|---|---|
id |
yes | the MCP tool name. Must not contain . (dots are reserved for namespace prefixing). Legacy manifests that use name are rejected with a message asking for id. |
title, description |
yes | description is what the model reads when choosing a tool. |
schema |
yes | path to a JSON-Schema file for the tool’s arguments, relative to the plugin root; must not contain ... A property carrying "format": "file-path" (or an array whose items do) is what ties that argument to the fs.read / fs.write parameter: true checks — its value at call time is the path the dispatcher grants and checks against the calling agent’s folder; a path passed under a property without that format gets no grant, so only a static rule could admit it. |
handler |
node only |
path to the handler module, relative to the plugin root; must not contain ... Required for runtime: "node", forbidden for "process". |
timeout |
no | e.g. "30s". |
fileExtensions[] |
no | extensions the tool applies to (used by the app’s context menus). |
Handler result shape. A node handler’s return value reaches the agent as one MCP text block: a string
verbatim, anything else JSON.stringify’d. A handler that returns an MCP content envelope —
{ content: [ { type: "text", text }, { type: "image", data: <base64>, mimeType: "image/…" } ] }, every block
well-formed — is forwarded verbatim, so an image it returns reaches the model as an image on providers that accept
image tool results. A malformed envelope falls back to the text wrapping;
nothing is dropped silently. One name, one tool: the agent loop registers built-ins first, then host tools, then
plugin tools, and a later tool whose name is already taken is skipped with a warning that names its source — a
plugin tool cannot shadow a built-in or a host tool of the same name (document-essentials renamed its reader to
read_text_document for this reason).
actions[] — right-click AI actions
Section titled “actions[] — right-click AI actions”| Field | Required | Rule |
|---|---|---|
id |
yes | kebab-case. |
title, description |
yes | shown in the context menu / action dialog. |
scope |
yes | "single-file", "multi-file", "folder", or an array of those. |
kind |
no | "prompt" (default): renders prompt and opens an agent turn in chat. "tool": invokes toolId directly — no agent turn, no model. |
prompt |
prompt kind |
path to the prompt template, relative to the plugin root; must not contain ... Forbidden for kind: "tool". |
toolId |
tool kind |
must match a contributes.tools[].id in the same manifest. |
toolScope |
no, prompt kind only |
"none" (no tools this turn) or "read-only" (only tools that read the folder and its knowledge — nothing that writes, runs a command, reaches the network, or belongs to a plugin). The manifest names a scope, never a tool; the host resolves it into the turn’s tool list and enforces it on the agent loop for that one turn. A prompt that says “read the file at …” needs read-only, not none — with no tools the model cannot read and will narrate instead. Forbidden for kind: "tool" (it runs no agent turn). |
fileExtensions[], fileTypes[] |
no | when the action is offered. |
multiFileMode |
no, tool kind |
"independent" (default): one tool call per selected file. "batch": exactly one call with every selected path under the tool schema’s array-of-paths property — for a tool whose point is to combine files. |
params[] |
no | dialog inputs: { id, label, type: "select" | "text" | "per-file-text", options?, placeholder?, default?, required? }. |
generate |
no | an in-form button { label, actionId, populateParamId, forwardParamIds? } that invokes a sibling kind: "tool" action (actionId must exist in the same manifest) and fills one of this action’s per-file-text params from its result. |
hidden |
no | true keeps the action out of the context menu while it stays invocable as another action’s generate.actionId. |
settings{} — the plugin’s settings card
Section titled “settings{} — the plugin’s settings card”Keyed by setting id. Each value: { type: "string" | "boolean" | "number" | "action" | "info", title, description?, default?, secret?, required?, enum?, multiline?, min?, max?, order?, handler?, variant? }. secret: true stores the
value in the plugin secret store (plugins/secrets.enc, encrypted with the OS keychain through Electron’s
safeStorage when available) and hands it to handlers through ctx.settings, never to the renderer in plain text. type: "action" renders a button that runs
handler (relative path); variant is "primary" | "secondary" | "danger". Not available under runtime: "process".
hooks{} — event handlers
Section titled “hooks{} — event handlers”Keyed by event name; each value is either a handler path or { handler, filter? }. Handler paths must not contain
... Event names, payloads and the filter shape are on Hooks and events (onAgentComplete, onSessionMessage, onToolCall, …). Not available under runtime: "process".
commands[] — slash commands
Section titled “commands[] — slash commands”{ id, title, icon?, contexts?[], handler?, when?, fileExtensions?[] }. handler must not contain ...
views[] — plugin-contributed panels
Section titled “views[] — plugin-contributed panels”{ id, title, icon?, location?, definition? }. Not available under runtime: "process".
permissions
Section titled “permissions”Everything a plugin may touch is declared here and enforced by the plugin context it receives (ctx). An
undeclared capability is not removed from ctx — the member is still there, but calling it throws a permission
error whose message names the plugin and the capability, with not-declared as the reason (settings.*, fs.*,
triggerInbound / listChannels / getSessionHistory / respondToHitl, renderHtmlToPdf, session.*,
ai.complete, channels.*). Two exceptions: ctx.toast and ctx.store are undefined when not declared, and
ctx.log without log: true silently does nothing — no error, no output. Hook handlers receive the same
wrapped ctx with an empty grant set and no calling-agent folder, so inside a hook parameter: true grants
nothing — only static rules (and deny) apply there.
| Key | Meaning |
|---|---|
fs.read / fs.write |
{ static?: string[], parameter?: boolean, deny?: string[] }. static — glob patterns the handler may touch on its own; parameter: true — the path comes from the call’s arguments (the schema property marked "format": "file-path") and is checked at dispatch against the calling agent’s folder; deny — always refused. The ctx.fs proxy (read, write, exists, …) is where static rules are enforced. Relative static patterns are resolved against the app process’s working directory, not the plugin’s directory — "./cache/**" does not mean the plugin’s own cache/; write "${dataDir}/cache/**" (the plugin’s data directory) or an absolute path. Patterns beginning with * (e.g. **/.env) are matched as globs without resolution. |
fs.list |
{ static?: string[] } — directory paths matched by prefix; same resolution rule as above, so use ${dataDir} or absolute paths, not "./data". |
fetch |
{ static?: string[], settings?: string[], deny?: string[] } — allowed hosts, hosts read from named settings, and a deny list; localhost/private ranges are refused regardless (SSRF guard). static / deny entries are host names. A settings entry names a setting whose value must be a full URL (https://api.example.com/v1) — the host is taken from it with a URL parse, so a bare host name stored in the setting parses as nothing and grants nothing. |
store |
true — a per-plugin key/value store. |
session |
{ getActive?, sendMessage? }. |
hostTrigger |
true — ctx.triggerInbound, routing a resolved inbound channel message to the host’s inbound trigger. Separate from fetch on purpose: it targets only the fixed host origin. |
ai |
{ complete?: true } — ctx.ai.complete, a single tool-free completion on the user’s own configured model. Not an agent turn; no HITL gating of its own. Inline images are read by the local vision models on the local route. |
channels |
{ getIntegrationConfig?, setIntegrationConfig? }. |
settings, toast, renderHtmlToPdf, log |
true to expose the corresponding ctx member. |
execution |
{ timeout?, maxTimeout? } — handler time budget. |
Trust and allow-lists
Section titled “Trust and allow-lists”A plugin inherits the folder agent’s trust level for approval gating; a folder’s settings.json may carry a plugin
allow-list, and a tool from a plugin not on it is refused at the Plugin MCP Server with a message that names the
plugin.
