Custom tools
Add agent tools exposed over MCP so your AI agent can call domain-specific functions — market data fetchers, screeners, notification hooks, and more.
Custom tools are functions the agent can call during the research and decision loop, exposed over the Model Context Protocol (MCP) so any MCP-compatible agent — Claude Code or others — can discover and use them.
How MCP tools work in Qoc
Qoc runs an embedded MCP server that the agent connects to at startup. When you register a custom tool, it appears in the agent's tool list alongside built-in Qoc tools (read-position, get-quote, propose-order, etc.).
A tool definition has three parts: a name (snake_case, globally unique within your workspace), an input schema (JSON Schema describing the arguments), and a handler (an async function that runs on the Qoc host and returns a result string).
Handlers run in the same process as the Qoc runtime and have access to the workspace filesystem and any secrets you pass through ctx. They do not run inside the agent's sandbox.
Tool definition interface
import type { JsonSchema, ToolContext } from "@qoc-app/sdk";
export interface ToolDefinition {
/** snake_case name the agent uses to call this tool */
name: string;
/** One-sentence description shown to the agent in its tool list */
description: string;
/** JSON Schema for the arguments object */
inputSchema: JsonSchema;
/** Handler called with parsed, validated arguments */
handler(args: Record<string, unknown>, ctx: ToolContext): Promise<string>;
}Example: sector-exposure tool
import type { ToolDefinition, ToolContext } from "@qoc-app/sdk";
const sectorExposureTool: ToolDefinition = {
name: "get_sector_exposure",
description:
"Return the current portfolio weight (pct of NAV) for each GICS sector, " +
"derived from current positions.",
inputSchema: {
type: "object",
properties: {
min_weight: {
type: "number",
description: "Only return sectors with weight above this threshold (0-100)",
default: 0,
},
},
required: [],
},
async handler(args, ctx: ToolContext): Promise<string> {
const minWeight = typeof args.min_weight === "number" ? args.min_weight : 0;
const positions = await ctx.getPositions();
const nav = await ctx.getNAV();
// group by sector (sector data comes from a local CSV in the workspace)
const sectorMap: Record<string, number> = {};
for (const pos of positions) {
const sector = await ctx.resolveMeta(pos.symbol, "sector");
const notional = pos.quantity * pos.mark;
sectorMap[sector] = (sectorMap[sector] ?? 0) + notional;
}
const rows = Object.entries(sectorMap)
.map(([sector, notional]) => ({
sector,
weight_pct: Number(((notional / nav) * 100).toFixed(2)),
}))
.filter((r) => r.weight_pct >= minWeight)
.sort((a, b) => b.weight_pct - a.weight_pct);
return JSON.stringify(rows, null, 2);
},
};
export default sectorExposureTool;Registering the tool in desk.toml
[[tool]]
name = "get_sector_exposure"
adapter = "./src/tools/sector-exposure.ts"
enabled = trueTool output format
Tool handlers must return a plain string. JSON is the most useful format because the agent can parse and reason over structured data. Keep responses concise — the agent has a finite context window, and verbose tool output crowds out other reasoning.
If your tool can fail (network call, missing data), throw an Error with a descriptive message. Qoc wraps the error into a structured tool result so the agent sees error: <message> and can decide how to proceed.
Tools are not guards
A tool called by the agent during research does not block order placement. If you want to enforce a constraint on every order, implement it as a custom guard, not a tool. Tools are advisory; guards are mandatory.
Test tools directly with qoc run
Run qoc run tool get_sector_exposure --args '{"min_weight": 5}' to invoke the tool directly from the CLI without starting the agent. This is the fastest way to iterate on handler logic.