Skip to content
Minnow
GitHub
Download

Native tool plugin authoring

Minnow can load local tool plugins from ~/.minnow/tools/<pluginId>/ without running a separate MCP server. Each pack contains a JSON manifest and a single JavaScript handler executed on the Node tool server (npm start).

Layout

~/.minnow/tools/<pluginId>/
  tool.json       # metadata + OpenAI parameters schema
  handler.mjs     # export default async function handler(args, ctx)

The folder name must match tool.jsonid. Plugin ids use lowercase letters, numbers, and hyphens (^[a-z0-9][a-z0-9-]*$).

Exposed tool name

The model sees:

plugin__<pluginId>__<functionName>

Hyphens in the plugin id become underscores in the namespace segment only. Example: id hello-world + function greetplugin__hello_world__greet.

tool.json

See documentation/schemas/tool-plugin.schema.json.

Required fields: id, functionName, label, description, category, parameters (JSON Schema object).

Optional capabilities (v1 metadata for future gating):

FieldDefaultMeaning
filesystemfalseDeclares filesystem access needs
networkfalseDeclares network access needs
nodeBuiltinfalseReserved for trusted Node builtins

handler.mjs

/**
 * @param {Record<string, unknown>} args — arguments from the model
 * @param {import('../../server/tools/plugin-context.js').PluginContext} ctx
 * @returns {Promise<string>} — result text (use "Error: …" for failures)
 */
export default async function handler(args, ctx) {
  return `Hello, ${args.name ?? 'world'}`;
}

ctx provides:

  • workspaceRoot — active workspace path
  • minnowHome~/.minnow (or MINNOW_HOME)
  • log(message) — server log line
  • env — read-only NODE_ENV, MINNOW_HOME

Return a string from the handler. Uncaught errors are converted to Error: … strings by the server.

Enablement and permissions

~/.minnow/tools.json:

{
  "plugins": {
    "my-plugin": { "enabled": true }
  },
  "permissions": {
    "default": {
      "plugin__my_plugin__greet": "ask"
    }
  }
}
  • plugins.<id>.enabled: false removes the tool from GET /api/plugins/tools and from chat tool lists.
  • permissions.default.<namespacedName> uses full, ask, or off (default for new plugin tools is ask).

Configure in Settings → Tools → Plugins, or edit tools.json directly.

APIs

MethodPathPurpose
GET/api/plugins/pingHealth check
GET/api/pluginsList plugin metadata
GET/api/plugins/toolsOpenAI function definitions (enabled packs)
POST/api/plugins/reloadRescan packs and clear handler cache
POST/api/plugins/scaffoldCopy _template to ~/.minnow/tools/<id>/

Execution uses the same path as built-in server tools: POST /api/tools with { "name": "plugin__…", "args": {} }.

Scaffold

With npm start running:

curl -X POST http://localhost:9473/api/plugins/scaffold \
  -H 'Content-Type: application/json' \
  -d '{"id":"demo"}'

Or use Scaffold new plugin… in Settings → Tools → Plugins.

Sandbox and trust

By default, handlers run in a Node vm wrapper with a minimal sandbox (args, ctx, limited console.log). For local development only, set MINNOW_PLUGIN_UNSAFE=1 to load handlers via full dynamic import() (same OS user as the dev server — only use on trusted code).

Comparison with MCP

MCPNative plugin
ProcessSeparate stdio serverIn-process handler
Namingmcp__server__toolplugin__id__function
Best forExisting MCP toolsQuick local extensions

Tests

npm run test:plugins

Fixtures live under test/fixtures/plugin-tools/echo/ (plugin__echo__pingpong).

---