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
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
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
{
"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 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.
// 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
// 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
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Unique plugin identifier. Used as the tool name prefix. |
version | string | Yes | Semver string. Logged at startup. |
description | string | Yes | Human-readable description shown in plugin list. |
configSchema | JSONSchema | No | JSON Schema for plugin config. Validated at startup. |
tools | ToolDefinition[] | Yes | Array of tool definitions (see below). |
onLoad | async (config) => void | No | Lifecycle hook called once at plugin load. Good for DB connections. |
onUnload | async () => void | No | Called when Conductor shuts down. Close connections here. |
name | string | Yes | Full tool name e.g. pluginName.action. Dot-separated. |
description | string | Yes | Shown to the AI. Be specific — this is how the AI decides whether to use it. |
inputSchema | JSONSchema | Yes | Validated before handler runs. Use required[] for mandatory fields. |
requiresApproval | boolean | No | If true, user must confirm before handler runs. Default: false. |
handler | async (input, config) => CallToolResult | Yes | The actual implementation. Return { content: [{ type: 'text', text: '...' }] }. |
Testing your plugin
conductor plugins listConfirm your plugin appears with status: enabledconductor plugins validate my-pluginValidate config schema and tool definitionsconductor doctorFull system check including plugin healthconductor mcp start --log-level debugVerbose mode — see every tool call and validation error