Skip to main content

Tools

Tools are how your plugin talks to AI agents. Each tool is a named, typed function with a Zod input schema, a Zod output schema, and a handler that runs directly in the browser page — with full access to your authenticated session. The agent calls it, the platform validates the input, and your handler executes.

Import

import { defineTool } from '@opentabs-dev/plugin-sdk';
import type { ToolDefinition, ToolHandlerContext } from '@opentabs-dev/plugin-sdk';

defineTool(config)

Type-safe factory function that returns a ToolDefinition. It's an identity function — no runtime behavior, just generic inference for TypeScript.

function defineTool<
  TInput extends z.ZodObject<z.ZodRawShape>,
  TOutput extends z.ZodType,
>(config: ToolDefinition<TInput, TOutput>): ToolDefinition<TInput, TOutput>;

ToolDefinition

interface ToolDefinition<
  TInput extends z.ZodObject<z.ZodRawShape>,
  TOutput extends z.ZodType,
> {
  name: string;
  displayName?: string;
  description: string;
  summary?: string;
  icon?: LucideIconName;
  group?: string;
  input: TInput;
  output: TOutput;
  handle(params: z.infer<TInput>, context?: ToolHandlerContext): Promise<z.infer<TOutput>>;
}

Properties

PropertyTypeDescription
namestringTool name in snake_case (e.g., 'send_message'). Auto-prefixed with the plugin name for MCP registration (slack_send_message).
displayNamestringHuman-readable name shown in the side panel (e.g., 'Send Message'). Optional — auto-derived from name during build when omitted.
descriptionstringDescription shown to AI agents. Be specific — this is how the agent decides when to use the tool.
summarystringShort human-readable summary for the UI. Falls back to description if omitted. Useful when the full description is too verbose for compact UI layouts.
iconLucideIconNameLucide icon in kebab-case (e.g., 'message-square'). Displayed in the side panel. Optional — defaults to 'wrench' during build when omitted. See lucide.dev/icons.
groupstringOptional group name for visual grouping in the side panel. Tools with the same group render together under a section header.
inputz.ZodObjectZod object schema for tool input. Serialized to JSON Schema for MCP registration and used for server-side validation.
outputz.ZodTypeZod schema describing the output shape. Used for manifest generation and MCP tool registration.
handle(params, context?) => PromiseAsync handler that executes in the browser page context. Input is pre-validated against the input schema. The optional context parameter provides progress reporting.

ToolHandlerContext

Optional second argument to handle(), injected by the adapter runtime. Provides progress reporting for long-running operations.

interface ToolHandlerContext {
  reportProgress(opts: ProgressOptions): void;
}
 
interface ProgressOptions {
  progress?: number;  // Current value (e.g., 3 of 10). Omit for indeterminate progress.
  total?: number;     // Total expected value (e.g., 10). Omit for indeterminate progress.
  message?: string;   // Human-readable status (e.g., 'Processing page 3')
}

The context parameter is optional — tools that don't need progress reporting can omit it. Always use optional chaining (context?.reportProgress(...)) for safety. See Streaming & Progress for the full guide.

Example

import { defineTool, postJSON, ToolError } from '@opentabs-dev/plugin-sdk';
import type { ToolHandlerContext } from '@opentabs-dev/plugin-sdk';
import { z } from 'zod';
 
export const sendMessage = defineTool({
  name: 'send_message',
  displayName: 'Send Message',
  description: 'Send a message to a Slack channel by name or ID.',
  icon: 'message-square',
 
  input: z.object({
    channel: z.string().describe('Channel name or ID'),
    text: z.string().describe('Message text'),
  }),
 
  output: z.object({
    ok: z.boolean(),
    ts: z.string().describe('Message timestamp ID'),
  }),
 
  async handle(params, context?: ToolHandlerContext) {
    const result = await postJSON<{ ok: boolean; ts: string }>(
      '/api/chat.postMessage',
      { channel: params.channel, text: params.text },
    );
 
    if (!result || !result.ok) {
      throw ToolError.internal('Failed to send message');
    }
 
    return { ok: result.ok, ts: result.ts };
  },
});

Zod Schema Rules

Tool schemas are serialized to JSON Schema via z.toJSONSchema() at build time. Keep schemas serialization-compatible:

  • No .transform() — transforms cannot be represented in JSON Schema. Normalize input in handle() instead.
  • No .pipe() or .preprocess() — these produce runtime-only behavior that breaks serialization.
  • .refine() callbacks must not throw — Zod 4 runs refine callbacks even when the base validator fails. Wrap any throwing logic in try-catch and return false on error.
  • Stick to declarative types — primitives, objects, arrays, unions, literals, enums, and standard validations (.min(), .max(), .url(), etc.) serialize cleanly.

Name Prefixing

Tool names are automatically prefixed with the plugin name when registered with the MCP server. A tool named send_message in a plugin named slack becomes slack_send_message at the MCP level. You define just the unprefixed name.

Tool names must match ^[a-z][a-z0-9]*(_[a-z0-9]+)*$ — lowercase snake_case, starting with a letter.

Last Updated: 10 Mar, 2026