Tool registration and schemas
Entry point and tool schemas
The entry exports a synchronous or asynchronous register(ctx):
import { defineTool } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";
const result = (text) => ({
content: [{ type: "text", text }],
details: {}
});
export function register(ctx) {
ctx.registerTool(defineTool({
name: "MyTool",
label: "Example tool",
description: "Returns the supplied text.",
parameters: Type.Object({
value: Type.String({ description: "Text to return." })
}),
execute: async (_callId, params) => {
if (typeof params.value !== "string") {
return result("value must be a string");
}
return result(params.value);
}
}));
ctx.logger.info("example tool registered");
}
Use PI's defineTool and TypeBox schemas. The schema is the tool's input
contract exposed to the model. Use the TypeBox constructors for objects,
strings, numbers, booleans, arrays, unions, optionals, and literals. Validate
data from external systems again inside the handler. A normal result has a
content array containing text or other PI-supported content and a
details object for host-only metadata.
A failure the model cannot fix by calling differently is thrown rather than
returned as text: the host turns a throw into a tool result flagged
is_error. That is every transport and host fault (a network, DNS or socket
error, an origin answering 5xx, a dead remote transport), never a bad
argument, a missing resource or a policy refusal, which stay text results the
model can act on. Use markTransportFailure(error) from
elowen-plugin-shared/transport to tag such a failure and isTransportFailure
in the tool's own catch-all so it is rethrown instead of flattened into an
answer.
registerTool(tool, { external: true }) marks an externally authored schema.
The host validates this boolean and normalizes an omitted value to false for
all registrations. PluginToolMetadata is the normalized host-side type; tool
names never imply origin, and an explicit false stays internal even with an
mcp__ prefix. The bundled MCP bridge supplies external: true, including its
snapshot-backed runner registrations. Metadata follows the accepted tool owner
through registry merges; a rejected collision contributes neither origin nor
read-only classification.
During session composition, external descriptions are capped at 1,000 UTF-8
bytes and definitions above 8,000 serialized bytes receive a permissive object
parameter schema with an omission notice. The external owner remains the
argument validator. External schemas are never reconstructed to add reason;
internal tools receive the normal status field except the fixed Bash and
ToolSearch exclusions. Origin classification is independent of deferral.
Cache diagnostics receive a separate projection of external names and redact
both individual names and hosted namespaces containing external tools. This
metadata is never inserted into provider payloads. Work is linear in the tool
catalog plus the external schemas inspected at composition.
A declared provides.readOnlyTools: ['mcp__*'] expands only to that plugin's
registered external tools and requires the matching provides.tools entry.
Exact read-only names retain their own declaration and ownership checks. Neither
form grants tool execution access or changes account wildcard grammar, plan-mode
policy, output artifact selection, automatic image previews or locked deferral.
Without external registrations, local schemas and diagnostic names stay local.
registerTool(tool, { ownerUserId, projectId, platform }) can scope a contribution
to one account, one Project, or conversations from a named platform. Without
these options the contribution is available instance-wide. A platform-scoped
tool is omitted from other sessions' model tool lists and catalog, not merely
rejected when called. This is useful for tools that require a visitor's page or
another platform-specific turn. Platform scoping does not replace execution-time
authorization. Resolve identity and access at execution time:
const identity = ctx.currentIdentity();
const access = ctx.currentAccess();
const accountUserId = ctx.currentAccountUserId();
Do not capture those values during registration or reuse them across turns.
currentAccess().readOnly carries both an admitted Plan restriction and the immutable delegated read-only origin. An imposed delegated origin also keeps planMode true for further delegation even though the child itself starts in Build and receives no owner-chat mode directive. Plugins must never clear either restriction when constructing child access.
currentAccess().admin and projectIds are the live effective policy scope, intersected with
the session's captured ceiling. Account demotion and project revocation apply without respawning;
later grants cannot widen a previously scoped session. File and directory tools consume the same
host policy. currentIdentity() also removes captured admin/owner privilege when live host authority
is revoked; it never promotes a turn. Read access immediately before each operation, including after
an asynchronous wait. Queued host Write/Edit recheck the anchored path inside the mutation queue.
Tool calls still pass through the host permission boundary, plan-mode policy,
plugin grant checks, and applicable hooks.
The manifest metadata completes the runtime tool definition:
- Successful output is hidden unless
showOutputmatches the tool. - Plan mode advertises the core read-only tools plus exact names in
planSafe. An undeclared plugin tool is not plan-safe. deferLoadingdefers matching tools even in a small session. This is useful for optional or large tool families.planSafedoes not keep a tool loaded: plan mode reaches deferred read-only tools through the loader like any other turn.- A tool name in
provides.toolsis an audit declaration. The actual registration remains the source of the live contribution report.
Owning code and consumers
- The
PluginContextmethods and the registration option types live insrc/plugins/api.ts:PluginToolRegistrationOptionsat line 2370 (ownerUserId,projectId,platform,external),registerToolat line 2398,currentAccessat line 2773,currentIdentityat line 2783,currentAccountUserIdat line 2797, andloggerat line 3048. - The manifest fields
showOutput,planSafeanddeferLoadingare declared insrc/plugins/manifest.tsat lines 147 to 149.provides.readOnlyToolsis at line 134 and its validation at lines 291 to 293. - The external caps (1,000 UTF-8 bytes for descriptions, 8,000 serialized
bytes for definitions) are the constants in
src/brain/toolSchemaCap.tsat lines 23 and 27. markTransportFailureandisTransportFailureare inpackages/plugin-shared/transport.mjsat lines 20 and 28, exported aselowen-plugin-shared/transport.- The bundled MCP bridge is the real consumer of
external: true; seeplugins/mcp/index.mjsat line 678.
Absent behavior, limits and cost
- A tool that is never registered is absent from the model's tool list.
Manifest lists in
providesare audit declarations and do not register anything. - External schema and description limits are applied when the session is composed. The object-root rule for local schemas, and what happens to a refused schema, are in row 1 of the seam catalog.
- A tool's schema and description are part of the provider request, so their size costs input tokens on every turn that offers the tool.