A tool is a function the agent calls with validated args. Return one from getTools (boot-time) or getRequestTools (per-request).
Build a PluginTool with the tool() helper
Use the tool() builder from @ixo/oracle-runtime. The handler gets validated args and a per-request RuntimeContext.
import { tool, z, type PluginTool, type RuntimeContext } from '@ixo/oracle-runtime';
export function buildCurrentWeatherTool(): PluginTool {
return tool(
async (args, ctx: RuntimeContext) => {
const { city } = z.object({ city: z.string().min(1) }).parse(args);
ctx.logger.log(`weather lookup by ${ctx.user.did} for ${city}`);
const result = await fetchWeather(city, ctx.abortSignal);
if (!result) return `Could not find weather for "${city}".`;
return JSON.stringify(result);
},
{
name: 'get_current_weather',
description: 'Get the current weather for a city.',
schema: z.object({
city: z.string().min(1).describe('City name, e.g. "Berlin".'),
}),
},
);
}Canonical source: weather-tools.ts.
Return it from getTools(ctx) for the stable case
Pick getTools when the tool list is the same for every user — it only depends on config and identity.
import { OraclePlugin, type PluginContext, type PluginTool } from '@ixo/oracle-runtime';
export class WeatherPlugin extends OraclePlugin {
override getTools(ctx: PluginContext): PluginTool[] {
return [buildCurrentWeatherTool()];
}
}See getTools in weather.plugin.ts.
Return it from getRequestTools(rtCtx) when it varies per request
Use getRequestTools when the tool list depends on live state — the user, session, history, or loadedPlugins.
override getRequestTools(rtCtx: RuntimeContext): PluginTool[] {
const tz = rtCtx.user.timezone ?? 'auto';
return [buildForecastTool(tz)];
}Both hooks merge — their outputs are concatenated on every build. See getRequestTools in weather.plugin.ts.
Override visibility per tool (optional)
A tool inherits its plugin's manifest.visibility by default. Add a visibility field to the PluginTool to override per-tool.
const currentTool = buildCurrentWeatherTool();
currentTool.visibility = 'always';
const forecastTool = buildForecastTool();
forecastTool.visibility = 'on-demand';
return [currentTool, forecastTool];See Set visibility for the full recipe.
What to know before shipping
- Tool
namemust be unique across all loaded plugins. Boot fails on collision. - Throw from the handler for unexpected errors (retry middleware catches them). Return a string for expected misses ("not found").
- The agent expects a string;
JSON.stringifystructured output to keep the contract clean. ctx.abortSignalpropagates from the inbound request — forward it to everyfetch.- Do not override descriptions of upstream MCP tools — pass them through verbatim and put guidance in the manifest instead.