Skip to content

Cookbook: REST API CRUD Tools

A pattern for wrapping a standard REST API as plugin tools.

Pattern

typescript
import { definePlugin, z, type ToolDefinition } from '@hivemind-os/plugin-sdk';

// Reusable API helper
async function api(ctx: any, path: string, opts?: { method?: string; body?: unknown }) {
  const res = await fetch(`${ctx.config.baseUrl}${path}`, {
    method: opts?.method ?? 'GET',
    headers: {
      Authorization: `Bearer ${ctx.config.apiKey}`,
      'Content-Type': 'application/json',
    },
    body: opts?.body ? JSON.stringify(opts.body) : undefined,
  });
  if (!res.ok) throw new Error(`API error: ${res.status}`);
  return res.json();
}

// List tool (read-only)
const listItems: ToolDefinition = {
  name: 'list_items',
  description: 'List items with optional filters',
  parameters: z.object({
    status: z.enum(['active', 'archived', 'all']).default('active'),
    limit: z.number().min(1).max(100).default(20),
  }),
  execute: async (params, ctx) => {
    const items = await api(ctx, `/items?status=${params.status}&limit=${params.limit}`);
    return { content: items };
  },
};

// Get tool (read-only)
const getItem: ToolDefinition = {
  name: 'get_item',
  description: 'Get a single item by ID',
  parameters: z.object({
    id: z.string().describe('Item ID'),
  }),
  execute: async (params, ctx) => {
    return { content: await api(ctx, `/items/${params.id}`) };
  },
};

// Create tool (side-effect)
const createItem: ToolDefinition = {
  name: 'create_item',
  description: 'Create a new item',
  parameters: z.object({
    title: z.string(),
    description: z.string().optional(),
  }),
  annotations: { sideEffects: true, approval: 'suggest' },
  execute: async (params, ctx) => {
    const item = await api(ctx, '/items', { method: 'POST', body: params });
    await ctx.emitEvent('item.created', { id: item.id });
    return { content: item };
  },
};

// Update tool (side-effect)
const updateItem: ToolDefinition = {
  name: 'update_item',
  description: 'Update an existing item',
  parameters: z.object({
    id: z.string(),
    title: z.string().optional(),
    status: z.enum(['active', 'archived']).optional(),
  }),
  annotations: { sideEffects: true, approval: 'suggest' },
  execute: async (params, ctx) => {
    const { id, ...body } = params;
    return { content: await api(ctx, `/items/${id}`, { method: 'PATCH', body }) };
  },
};

// Delete tool (side-effect, always require approval)
const deleteItem: ToolDefinition = {
  name: 'delete_item',
  description: 'Delete an item permanently',
  parameters: z.object({
    id: z.string().describe('Item ID to delete'),
  }),
  annotations: { sideEffects: true, approval: 'always' },
  execute: async (params, ctx) => {
    await api(ctx, `/items/${params.id}`, { method: 'DELETE' });
    return { content: `Item ${params.id} deleted` };
  },
};

export default definePlugin({
  configSchema: z.object({
    apiKey: z.string().secret().label('API Key').section('Auth'),
    baseUrl: z.string().default('https://api.myservice.com').label('Base URL'),
  }),
  tools: [listItems, getItem, createItem, updateItem, deleteItem],
});

Key Points

  • Read tools have no annotations (safe to auto-execute)
  • Write tools use sideEffects: true + approval: 'suggest'
  • Destructive tools use approval: 'always'
  • Emit events after mutations for workflow triggers
  • Reusable API helper reduces boilerplate

Released under the MIT License.