conductorv2

Guides

Writing custom plugins

Drop a .js file into ~/.conductor/plugins/ and Conductor loads it automatically at startup. No compilation, no registration step.

Minimal plugin

A plugin is a CommonJS module that exports a plain object with name, version, description, and tools. Each tool has a name, description, JSON Schema input spec, and an async handler.

~/.conductor/plugins/hello.js
// ~/.conductor/plugins/hello.js
module.exports = {
  name: "hello",
  version: "1.0.0",
  description: "A minimal example plugin",

  tools: [
    {
      name: "hello.greet",
      description: "Returns a greeting for the given name",
      inputSchema: {
        type: "object",
        properties: {
          name: { type: "string", description: "Name to greet" },
        },
        required: ["name"],
      },

      async handler({ name }) {
        return {
          content: [{ type: "text", text: `Hello, ${name}!` }],
        };
      },
    },
  ],
};

Restart Conductor (or run conductor plugins reload), then ask your AI: "use hello.greet with name Alex".

Plugin with configuration

Add a configSchema to declare what config your plugin needs. Conductor validates it at startup — if required fields are missing the plugin refuses to load with a clear error. The validated config is passed as the second argument to every handler.

~/.conductor/plugins/weather.js
// ~/.conductor/plugins/weather.js
module.exports = {
  name: "weather",
  version: "1.0.0",
  description: "Fetch current weather via OpenWeatherMap",

  // configSchema is validated at startup
  configSchema: {
    type: "object",
    properties: {
      apiKey: { type: "string" },
      units: { type: "string", enum: ["metric", "imperial"], default: "metric" },
    },
    required: ["apiKey"],
  },

  tools: [
    {
      name: "weather.current",
      description: "Get current weather for a city",
      inputSchema: {
        type: "object",
        properties: {
          city: { type: "string", description: "City name, e.g. London" },
        },
        required: ["city"],
      },

      // config is the validated plugin config
      async handler({ city }, config) {
        const url =
          `https://api.openweathermap.org/data/2.5/weather` +
          `?q=${encodeURIComponent(city)}&units=${config.units}` +
          `&appid=${config.apiKey}`;

        const res = await fetch(url);
        if (!res.ok) throw new Error(`Weather API error: ${res.status}`);

        const data = await res.json();
        return {
          content: [
            {
              type: "text",
              text: JSON.stringify({
                city: data.name,
                temp: `${data.main.temp}°`,
                condition: data.weather[0].description,
                humidity: `${data.main.humidity}%`,
              }, null, 2),
            },
          ],
        };
      },
    },
  ],
};
~/.conductor/config.json — add plugin config
// ~/.conductor/config.json
{
  "plugins": {
    "weather": {
      "enabled": true,
      "apiKey": "your-openweathermap-key",
      "units": "imperial"
    }
  }
}

Approval gates

Set requiresApproval: true on any tool that has destructive side effects. Conductor will halt execution and prompt the user before the handler runs. The AI waits for the response.

tool with requiresApproval
// Tool with approval gate
{
  name: "db.drop-table",
  description: "Drop a database table permanently",
  requiresApproval: true,  // user must confirm in terminal
  inputSchema: {
    type: "object",
    properties: {
      table: { type: "string" },
      database: { type: "string" },
    },
    required: ["table", "database"],
  },
  async handler({ table, database }, config) {
    const conn = await getConnection(config, database);
    await conn.query(`DROP TABLE IF EXISTS ${table}`);
    return { content: [{ type: "text", text: `Dropped ${table}` }] };
  },
}

Multiple tools in one plugin

Group related tools under one plugin. They share config, lifecycle, and context.

~/.conductor/plugins/notes.js
// Multiple related tools in one plugin
module.exports = {
  name: "notes",
  version: "1.0.0",
  description: "Local plaintext note storage",
  tools: [
    {
      name: "notes.write",
      description: "Save a note",
      requiresApproval: false,
      inputSchema: {
        type: "object",
        properties: {
          title: { type: "string" },
          content: { type: "string" },
        },
        required: ["title", "content"],
      },
      async handler({ title, content }) {
        const fs = require("fs/promises");
        const path = require("path");
        const dir = path.join(process.env.HOME, ".conductor", "notes");
        await fs.mkdir(dir, { recursive: true });
        await fs.writeFile(path.join(dir, `${title}.md`), content, "utf8");
        return { content: [{ type: "text", text: `Saved ${title}` }] };
      },
    },
    {
      name: "notes.read",
      description: "Read a saved note",
      inputSchema: {
        type: "object",
        properties: { title: { type: "string" } },
        required: ["title"],
      },
      async handler({ title }) {
        const fs = require("fs/promises");
        const path = require("path");
        const file = path.join(process.env.HOME, ".conductor", "notes", `${title}.md`);
        const content = await fs.readFile(file, "utf8");
        return { content: [{ type: "text", text: content }] };
      },
    },
    {
      name: "notes.list",
      description: "List all saved notes",
      inputSchema: { type: "object", properties: {} },
      async handler() {
        const fs = require("fs/promises");
        const path = require("path");
        const dir = path.join(process.env.HOME, ".conductor", "notes");
        const files = await fs.readdir(dir).catch(() => []);
        return { content: [{ type: "text", text: files.join("\n") || "No notes yet." }] };
      },
    },
  ],
};

TypeScript

TypeScript plugins are supported with the @useconductor/sdk package, which exports full types. Either pre-compile to JavaScript or use ts-node for development.

~/.conductor/plugins/myPlugin.ts
// ~/.conductor/plugins/myPlugin.ts
// Requires ts-node or pre-compilation
import type { Plugin, ToolDefinition } from "@useconductor/sdk";

interface Config {
  endpoint: string;
  apiKey: string;
}

const tools: ToolDefinition[] = [
  {
    name: "myPlugin.fetch",
    description: "Fetch data from the custom endpoint",
    inputSchema: {
      type: "object",
      properties: {
        path: { type: "string" },
      },
      required: ["path"],
    },
    async handler({ path }: { path: string }, config: Config) {
      const res = await fetch(`${config.endpoint}${path}`, {
        headers: { Authorization: `Bearer ${config.apiKey}` },
      });
      const data = await res.json();
      return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
    },
  },
];

const plugin: Plugin = {
  name: "myPlugin",
  version: "1.0.0",
  description: "Fetches from a custom API",
  configSchema: {
    type: "object",
    properties: {
      endpoint: { type: "string" },
      apiKey: { type: "string" },
    },
    required: ["endpoint", "apiKey"],
  },
  tools,
};

module.exports = plugin;

Plugin API reference

FieldTypeRequiredDescription
namestringYesUnique plugin identifier. Used as the tool name prefix.
versionstringYesSemver string. Logged at startup.
descriptionstringYesHuman-readable description shown in plugin list.
configSchemaJSONSchemaNoJSON Schema for plugin config. Validated at startup.
toolsToolDefinition[]YesArray of tool definitions (see below).
onLoadasync (config) => voidNoLifecycle hook called once at plugin load. Good for DB connections.
onUnloadasync () => voidNoCalled when Conductor shuts down. Close connections here.
ToolDefinition fields
namestringYesFull tool name e.g. pluginName.action. Dot-separated.
descriptionstringYesShown to the AI. Be specific — this is how the AI decides whether to use it.
inputSchemaJSONSchemaYesValidated before handler runs. Use required[] for mandatory fields.
requiresApprovalbooleanNoIf true, user must confirm before handler runs. Default: false.
handlerasync (input, config) => CallToolResultYesThe actual implementation. Return { content: [{ type: 'text', text: '...' }] }.

Testing your plugin

conductor plugins listConfirm your plugin appears with status: enabled
conductor plugins validate my-pluginValidate config schema and tool definitions
conductor doctorFull system check including plugin health
conductor mcp start --log-level debugVerbose mode — see every tool call and validation error