Skip to content

Plugins

Molf Assistant has an extensible plugin system for both the server and the worker. Plugins can add hooks, routes, tools, and services. Two plugins ship by default: @molf-ai/plugin-cron (server-side cron scheduling) and @molf-ai/plugin-mcp (worker-side MCP client integration).

Defining a Plugin

Use definePlugin from the protocol package to create a plugin descriptor:

typescript
import { definePlugin } from "@molf-ai/protocol";
import { z } from "zod";

export default definePlugin({
  name: "my-plugin",

  // Optional: validated config schema
  configSchema: z.object({
    interval: z.number().default(60),
  }),

  // Server-side initialization (optional)
  server(api) {
    api.on("turn_end", (event) => {
      api.log.info("Turn completed", { sessionId: event.sessionId });
    });

    // Return cleanup if needed
    return {
      destroy() {
        // Called on server shutdown
      },
    };
  },

  // Worker-side initialization (optional)
  worker(api) {
    api.addTool("my_tool", {
      description: "Does something",
      inputSchema: { type: "object", properties: {} },
      async execute(args) {
        return { output: "done" };
      },
    });
  },
});

PluginDescriptor

typescript
interface PluginDescriptor<TConfig = unknown> {
  name: string;
  configSchema?: ZodType<TConfig>;   // Zod schema for plugin config validation
  server?: (api: ServerPluginApi<TConfig>) => PluginCleanup | Promise<PluginCleanup>;
  worker?: (api: WorkerPluginApi<TConfig>) => PluginCleanup | Promise<PluginCleanup>;
}

A plugin can implement server, worker, or both. The configSchema is validated against the config provided in molf.yaml. Both init functions may return a { destroy() } cleanup object.

Plugin Configuration

Plugins are configured in molf.yaml:

yaml
plugins:
  # Simple specifier (no config)
  - "@molf-ai/plugin-cron"

  # With config
  - name: "my-plugin"
    config:
      interval: 30

The default plugins are ["@molf-ai/plugin-cron", "@molf-ai/plugin-mcp"].

Server Plugin API

The server(api) callback receives a ServerPluginApi with these capabilities:

Hook Registration

typescript
api.on("before_tool_call", (event) => {
  // Inspect or modify the event
  return { args: { ...event.args, modified: true } };
}, { priority: 10 });

See Hook System below for dispatch modes and available hooks.

Routes

Add tRPC-like routes accessible via the plugin.query and plugin.mutate procedures:

typescript
import { defineRoutes } from "@molf-ai/protocol";
import { z } from "zod";

const routes = defineRoutes({
  list: {
    type: "query",
    input: z.object({}),
    output: z.array(z.object({ id: z.string(), name: z.string() })),
    handler: async (input, ctx) => {
      return [{ id: "1", name: "example" }];
    },
  },
  create: {
    type: "mutation",
    input: z.object({ name: z.string() }),
    output: z.object({ id: z.string() }),
    handler: async (input, ctx) => {
      return { id: "new-id" };
    },
  },
});

// In server(api):
api.addRoutes(routes, context);

Clients call plugin routes via plugin.query({ plugin: "my-plugin", method: "list", input: {} }).

For typed client-side access, use createPluginClient:

typescript
import { createPluginClient } from "@molf-ai/protocol";

const client = createPluginClient("my-plugin", trpc, routes);
const items = await client.list({});

Tools

typescript
// Global tool (available in all sessions)
api.addTool("my_tool", toolDefinition);

// Per-session tool (factory called for each session)
api.addSessionTool((ctx) => {
  // ctx: { sessionId, workerId, workspaceId }
  return {
    name: "session_tool",
    toolDef: { /* ... */ },
  };
  // Return null to skip for this session
});

Services

Long-running services that start after all plugins are loaded and stop in reverse order on shutdown:

typescript
api.addService({
  async start() {
    // Initialize background work
  },
  async stop() {
    // Clean up
  },
});

Other API Members

MemberDescription
api.logScoped logger (debug, info, warn, error)
api.configValidated plugin config (typed if configSchema is provided)
api.dataPath(workerId?, workspaceId?)Scoped data directory under plugins/{pluginName}/
api.serverDataDirRaw server data directory (escape hatch)
api.sessionMgrSessionManager instance
api.eventBusEventBus instance
api.agentRunnerAgentRunner instance
api.connectionRegistryConnectionRegistry instance
api.workspaceStoreWorkspaceStore instance
api.workspaceNotifierWorkspaceNotifier instance

Worker Plugin API

The worker(api) callback receives a WorkerPluginApi:

typescript
// Add/remove tools
api.addTool("tool_name", {
  description: "...",
  inputSchema: { /* JSON Schema */ },
  async execute(args, ctx) {
    return { output: "result" };
  },
});
api.removeTool("tool_name");

// Add skills and agents
api.addSkill({ name: "my-skill", description: "...", content: "..." });
api.addAgent({ name: "my-agent", description: "...", content: "...", permission: {}, maxSteps: 10 });

// Sync state to server (after adding/removing tools)
await api.syncState();

// Hook registration
api.on("before_tool_execute", (event) => { /* ... */ });
MemberDescription
api.logScoped logger
api.configValidated plugin config
api.workdirWorker's working directory

Plugin Loading on Workers

Workers do not configure plugins directly. On registration, the server sends plugin specifiers to the worker in the plugins field of the registration response. The worker imports each plugin and calls descriptor.worker(api).

Hook System

Hooks use two dispatch modes:

Modifying Dispatch

Handlers run sequentially, sorted by priority (higher priority numbers run first, default is 0). Each handler sees the accumulated modifications from prior handlers. Handlers can return a partial object to modify the event data, or { block: "reason" } on blockable hooks to cancel the action.

typescript
api.on("before_prompt", (event) => {
  // Modify the system prompt
  return { systemPrompt: event.systemPrompt + "\nExtra instructions." };
}, { priority: 10 });

Only keys present in the original event data are merged from handler results.

Observing Dispatch

All handlers fire in parallel. Fire-and-forget -- errors are logged but do not affect the caller.

typescript
api.on("turn_end", (event) => {
  // Log or record metrics; cannot modify anything
});

Blocking

Three hooks support blocking: before_tool_call, before_compaction, and before_tool_execute. Returning { block: "reason" } from a handler on these hooks cancels the action. On non-blockable hooks, block results are ignored with a warning.

typescript
api.on("before_tool_call", (event) => {
  if (event.toolName === "dangerous_tool") {
    return { block: "Tool is not allowed" };
  }
});

Server Hooks

HookModeBlockableEvent Data
turn_startobservingnosessionId, prompt, model
before_promptmodifyingnosessionId, systemPrompt, messages, model, tools
after_promptobservingnosessionId, response, usage, duration
turn_endobservingnosessionId, message, toolCallCount, stepCount, duration
before_tool_callmodifyingyessessionId, toolCallId, toolName, args, workerId
after_tool_callmodifyingnosessionId, toolCallId, toolName, args, result, duration
before_compactionmodifyingyessessionId, messages, reason
after_compactionobservingnosessionId, originalCount, compactedCount, summary
session_createobservingnosessionId, name, workerId, workspaceId
session_deleteobservingnosessionId
session_savemodifyingnosessionId, messages
worker_connectobservingnoworkerId, name, tools, skills
worker_disconnectobservingnoworkerId, reason
server_startobservingnoport, dataDir
server_stopobservingno(empty)

Worker Hooks

HookModeBlockableEvent Data
before_tool_executemodifyingyestoolName, args, workdir
after_tool_executemodifyingnotoolName, args, result, duration
worker_startobservingnoworkerId, workdir
worker_stopobservingno(empty)

Built-in Plugin: Cron

The @molf-ai/plugin-cron plugin adds cron job scheduling to the server.

Routes

RouteTypeDescription
listqueryList cron jobs for a workspace
addmutationCreate a new cron job
removemutationDelete a cron job
updatemutationUpdate a cron job

Session Tool

Registers a per-session cron tool that lets the LLM manage cron jobs during a session.

Schedule Kinds

KindDescriptionFields
atOne-shot at a specific timeat (Unix timestamp in ms). Auto-removed after firing.
everyRepeating at a fixed intervalinterval_ms, optional anchor_ms
cronCron expressionexpr (cron string), optional tz (timezone)

Storage

Cron jobs are persisted at {dataDir}/workers/{workerId}/workspaces/{workspaceId}/cron/jobs.json.

Built-in Plugin: MCP

The @molf-ai/plugin-mcp plugin runs on the worker and integrates MCP (Model Context Protocol) servers as tools. See MCP Integration for configuration details.

See also