Skip to main content

Creating Tools

Learn the patterns and best practices for building CTP-compliant tools.

Tool Structure

Every CTP tool consists of two parts:
  1. Definition - Metadata describing the tool
  2. Function - Implementation that processes inputs
import type { ToolDefinition, ToolFunction } from '@conveniencepro/ctp-core';

// 1. Define result type
interface MyToolResult {
  // ... result properties
}

// 2. Create definition
export const myToolDefinition: ToolDefinition = {
  // ... metadata
};

// 3. Implement function
export const myToolFn: ToolFunction<MyToolResult> = (params) => {
  // ... implementation
};

// 4. Export together
export default { definition: myToolDefinition, fn: myToolFn };

Definition Patterns

Minimal Definition

export const minimalDefinition: ToolDefinition = {
  id: 'minimal-tool',
  name: 'Minimal Tool',
  description: 'A minimal example tool.',
  category: 'utilities',
  tags: ['example'],
  method: 'POST',
  parameters: [
    {
      name: 'input',
      type: 'text',
      label: 'Input',
      description: 'Input value',
      required: true,
    },
  ],
  outputDescription: 'Processed result',
  example: {
    input: { input: 'test' },
    output: { result: 'processed' },
  },
};
export const fullDefinition: ToolDefinition = {
  // Required
  id: 'advanced-tool',
  name: 'Advanced Tool',
  description: 'A fully-featured example with all options.',
  category: 'formatters',
  tags: ['advanced', 'example', 'featured'],
  method: 'POST',
  parameters: [/* ... */],
  outputDescription: 'Formatted output with metadata',
  example: {
    input: { /* ... */ },
    output: { /* ... */ },
  },

  // Optional metadata
  version: '1.0.0',
  icon: '🔧',
  keywords: ['full', 'complete'],
  relatedTools: ['simple-tool', 'other-tool'],

  // AI guidance
  aiInstructions: 'Use default settings unless user specifies otherwise.',

  // Execution
  executionMode: 'client',
  requiresAuth: false,

  // Rate limiting
  rateLimit: {
    requests: 100,
    window: 60,
  },
};

Implementation Patterns

Synchronous Tool

export const syncFn: ToolFunction<Result> = (params) => {
  const input = params.input as string;

  // Validation
  if (!input) {
    return {
      success: false,
      error: 'Input is required',
      errorCode: 'MISSING_REQUIRED',
    };
  }

  // Processing
  const result = processSync(input);

  // Return success
  return {
    success: true,
    data: result,
  };
};

Asynchronous Tool

export const asyncFn: ToolFunction<Result> = async (params) => {
  const input = params.input as string;

  try {
    // Async operation (e.g., Web Crypto)
    const hash = await crypto.subtle.digest(
      'SHA-256',
      new TextEncoder().encode(input)
    );

    return {
      success: true,
      data: { hash: arrayToHex(hash) },
    };
  } catch (error) {
    return {
      success: false,
      error: (error as Error).message,
      errorCode: 'EXECUTION_ERROR',
    };
  }
};

With Metadata

export const metadataFn: ToolFunction<Result> = (params) => {
  const startTime = performance.now();
  const input = params.input as string;

  const result = process(input);

  return {
    success: true,
    data: result,
    metadata: {
      executionTime: performance.now() - startTime,
      inputSize: input.length,
      outputSize: JSON.stringify(result).length,
    },
  };
};

With Warnings

export const warningFn: ToolFunction<Result> = (params) => {
  const algorithm = params.algorithm as string;
  const warnings: string[] = [];

  if (algorithm === 'SHA-1') {
    warnings.push('SHA-1 is deprecated for security purposes');
  }

  if (algorithm === 'MD5') {
    warnings.push('MD5 is cryptographically broken');
  }

  const result = compute(params);

  return {
    success: true,
    data: result,
    metadata: {
      warnings: warnings.length > 0 ? warnings : undefined,
    },
  };
};

Parameter Patterns

Select with Descriptions

{
  name: 'format',
  type: 'select',
  label: 'Output Format',
  description: 'Choose the output format',
  required: false,
  defaultValue: 'json',
  options: [
    { value: 'json', label: 'JSON', description: 'JavaScript Object Notation' },
    { value: 'yaml', label: 'YAML', description: 'Human-readable format' },
    { value: 'xml', label: 'XML', description: 'Extensible Markup Language' },
  ],
}

Conditional Parameters

parameters: [
  {
    name: 'mode',
    type: 'select',
    label: 'Mode',
    required: true,
    options: [
      { value: 'simple', label: 'Simple' },
      { value: 'advanced', label: 'Advanced' },
    ],
  },
  {
    name: 'advancedOption',
    type: 'text',
    label: 'Advanced Option',
    description: 'Only shown in advanced mode',
    required: false,
    dependsOn: [{ field: 'mode', condition: 'equals', value: 'advanced' }],
  },
]

Grouped Parameters

parameters: [
  // Input group
  { name: 'input', type: 'textarea', group: 'input', order: 1, /* ... */ },

  // Options group
  { name: 'format', type: 'select', group: 'options', order: 1, /* ... */ },
  { name: 'indent', type: 'number', group: 'options', order: 2, /* ... */ },

  // Advanced group
  { name: 'strict', type: 'boolean', group: 'advanced', order: 1, /* ... */ },
]

Bidirectional Tools

Handle encode/decode or similar operations:
export const bidirectionalDefinition: ToolDefinition = {
  id: 'base64-encoder',
  name: 'Base64 Encoder/Decoder',
  // ...
  parameters: [
    {
      name: 'input',
      type: 'textarea',
      label: 'Input',
      description: 'Text to encode or Base64 to decode',
      required: true,
    },
    {
      name: 'mode',
      type: 'select',
      label: 'Mode',
      required: false,
      defaultValue: 'encode',
      options: [
        { value: 'encode', label: 'Encode' },
        { value: 'decode', label: 'Decode' },
      ],
    },
  ],
};

export const bidirectionalFn: ToolFunction<Result> = (params) => {
  const input = params.input as string;
  const mode = (params.mode as 'encode' | 'decode') || 'encode';

  const output = mode === 'encode'
    ? btoa(input)
    : atob(input);

  return {
    success: true,
    data: { output, mode },
  };
};

Error Handling

Validation Errors

if (!input) {
  return {
    success: false,
    error: 'Input is required',
    errorCode: 'MISSING_REQUIRED',
  };
}

if (typeof input !== 'string') {
  return {
    success: false,
    error: 'Input must be a string',
    errorCode: 'TYPE_ERROR',
  };
}

if (input.length > 100000) {
  return {
    success: false,
    error: 'Input exceeds maximum length',
    errorCode: 'CONSTRAINT_VIOLATION',
  };
}

Execution Errors

try {
  const parsed = JSON.parse(input);
  return { success: true, data: { parsed } };
} catch (e) {
  return {
    success: false,
    error: `Invalid JSON: ${(e as Error).message}`,
    errorCode: 'INVALID_INPUT',
    suggestion: 'Check for missing quotes or trailing commas',
  };
}

Testing Tools

import { describe, it, expect } from 'vitest';
import myTool from './my-tool';

describe('my-tool', () => {
  it('returns success for valid input', () => {
    const result = myTool.fn({ input: 'test' });
    expect(result.success).toBe(true);
    expect(result.data).toBeDefined();
  });

  it('returns error for missing input', () => {
    const result = myTool.fn({});
    expect(result.success).toBe(false);
    expect(result.errorCode).toBe('MISSING_REQUIRED');
  });
});