Tools
Per-tool configuration, MCP annotations, and the read-only / destructive / open-world hints.
Tools can carry extra metadata that MCP clients use to decide whether to call them automatically. The ToolAnnotations interface (defined in src/types/index.ts) is the standard way to expose it.
The four annotations
| Field | Type | Meaning |
|---|---|---|
readOnlyHint | boolean | true means the tool doesn’t change any state. Clients can call it freely. |
destructiveHint | boolean | true means the tool mutates state in a way the user might want to confirm. |
openWorldHint | boolean | true means the tool talks to something beyond the local process (network, file system, etc.). |
title | string | A human-friendly title for the tool. Some clients render this instead of name. |
All four are optional. A tool with no annotations is treated as: read-write, possibly destructive, talks to the outside world, use the name as the title.
How a tool declares them
A typical example from src/tools/homeassistant/list-devices.tool.ts:
export class ListDevicesTool extends BaseTool<...> {
name = "list_devices";
description = "List Home Assistant devices, optionally filtered by domain or area.";
annotations: ToolAnnotations = {
readOnlyHint: true,
openWorldHint: true,
};
inputSchema = z.object({ ... });
async execute(input, context) { ... }
}
A control tool (e.g. control_light) gets:
annotations: ToolAnnotations = {
readOnlyHint: false,
destructiveHint: true, // could change device state the user didn't intend
openWorldHint: true,
};
A pure-computation tool that’s all in-process (e.g. a template tool that runs a Handlebars template against cached state) gets:
annotations: ToolAnnotations = {
readOnlyHint: true,
openWorldHint: false,
};
Why clients care
The MCP spec is deliberately quiet about how a client uses the annotations. In practice, the common interpretations are:
readOnlyHint: true: safe to call proactively when the model wants context (e.g. “list the lights so I can mention them in my answer”).destructiveHint: true: surface a confirmation prompt or require an explicit user instruction. Some clients will simply refuse to call these tools without a “yes”.openWorldHint: true: inform the model that the tool might fail in environment-specific ways (network down, token revoked, etc.). This is a hint to be more careful about error messages.
Set them honestly. If a tool reads from the network, mark it openWorldHint: true. If it deletes data, mark it destructiveHint: true. Over-marking trains users to dismiss the warnings.
Disabling a tool at runtime
The TOOL_<NAME>_DISABLED env var pattern lets you flip a tool off without redeploying. The check lives in src/mcp/MCPServer.registerTool:
if (process.env[`TOOL_${tool.name.toUpperCase()}_DISABLED`] === "true") {
logger.info(`Tool ${tool.name} disabled via env var`);
return;
}
For example, to disable the speech tools without removing them from the registry:
TOOL_TEXT_TO_SPEECH_DISABLED=true \
TOOL_VOICE_COMMAND_PARSER_DISABLED=true \
bun run start:stdio
The tool is omitted from the tools/list response, so clients can’t call it. Flip the env var back to remove the disable, no rebuild required.
Rate-limited tools
The RATE_LIMIT_MAX global applies to the /mcp endpoint, but a small number of expensive tools (e.g. analyze_audio, profile_devices) apply their own per-tool limit via a custom field on ToolAnnotations:
annotations: ToolAnnotations = {
// ...
custom: { rateLimit: { windowMs: 60_000, max: 5 } },
};
This is custom (not in the spec) and read by the per-tool rate-limit middleware. Five calls per minute is plenty for an interactive model and prevents a runaway loop from hammering HA.
Verifying the manifest
To see exactly what the running server advertises, hit POST /mcp with:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list"
}
The response includes each tool’s name, description, inputSchema, and annotations. If a tool is missing, it was disabled by env var (see above) or excluded from the registry (rebuild required).
Next
- Tools Reference > HA Tools — concrete examples of the annotation pattern across the 40+ tools.
- Architecture > Tool System — the underlying class structure.