npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@optimizely-opal/opal-tool-ocp-sdk

v1.1.6

Published

OCP SDK for Opal tool

Readme

OPAL TOOL OCP SDK

Optimizely Connect Platform (OCP) SDK for OPAL Tool

A TypeScript SDK for building Opal tools in Optimizely Connect Platform. This SDK provides decorators, abstractions, and utilities to simplify the development.

Features

  • 🎯 Decorator-based Tool Registration - Use @tool decorator to easily register functions
  • 🌐 Global and Regular Function Modes - SDK can be used in either global or organization-scoped mode
  • 🔧 Type-safe Development - Full TypeScript support with comprehensive type definitions
  • 🏗️ Abstract Base Classes - Extend ToolFunction or GlobalToolFunction for standardized request processing
  • 🔐 Authentication Support - OptiID authentication (default), custom auth providers supported
  • 🛡️ Authorization Support - OptiID token tool authorization
  • 📝 Parameter Validation - Define and validate tool parameters with types
  • Automatic Validation - SDK automatically validates parameters and returns RFC 9457 compliant error responses
  • 🧪 Comprehensive Testing - Fully tested with Jest

Quick Start

The SDK extends the functionality of OCP apps. You need to have an OCP app to use this SDK. Learn how to get started with OCP app development here.

Start by adding the SDK to your existing OCP app. In your OCP app folder, execute:

yarn add @optimizely-opal/opal-tool-ocp-sdk

To add an Opal too registry to your OCP app, add a function to your app manifest and mark it as an Opal tool function:

app.yml

functions: 
  opal_tool: # A unique key for your function. 
    entry_point: OpalToolFunction # The name of the class implementing your tool. 
    description: Opal tool function # A brief description of this function.
    opal_tool: true

Next, create and implement function class - both file name and class must match the value of entry_point property from app manifest. Function class must extend either ToolFunction or GlobalToolFunction class from @optimizely-opal/opal-tool-ocp-sdk (See 'Function modes' section below).

src/functions/OpalToolFunction

import { ToolFunction } from '@optimizely-opal/opal-tool-ocp-sdk';

export class OpalToolFunction extends ToolFunction {

}

Next, implement tools methods and annotate them with @tool decorator. Each such method is a tool in your registry. You can define mulitple tool methods in your app. Tool methods can be defined either as tool class instance methods or in separate classes. If tools are defined in separate classes, these classes need to be imported into the function class.

src/functions/OpalToolFunction

import { ToolFunction } from '@optimizely-opal/opal-tool-ocp-sdk';

export class OpalToolFunction extends ToolFunction {

  @tool({
    name: 'create_task',
    description: 'Creates a new task in the system',
    endpoint: '/create-task',
    parameters: [
      {
        name: 'title',
        type: ParameterType.String,
        description: 'The task title',
        required: true
      },
      {
        name: 'priority',
        type: ParameterType.String,
        description: 'Task priority level',
        required: false
      }
    ]
  })
  async createTask(params: { title: string; priority?: string }, authData: OptiIdAuthData) {
    return {
      id: '123',
      title: params.title,
      priority: params.priority || 'medium'
    };
  }
}

The format of params attribute matches the parameters defined in @tool decorator.

When the function is called by Opal, the SDK automatically:

  • Routes requests to your registered tools based on endpoints
  • Handles authentication and OptiID token validation before calling your methods
  • Provides discovery at /discovery endpoint for OCP platform integration
  • Returns proper HTTP responses with correct status codes and JSON formatting

Optionally, implement ready method to define when tool registry can be registered and called by Opal. OCP will call this method to show tool readiness in the UI. Only functions marked as ready can be registered and called by Opal. The value of reason property from the response will be displayed in OCP UI to inform users why the tool is not ready to be registered.

  protected override async ready(): Promise<ReadyResponse> {
    // validation logic
    if (!isValid) {
      return { ready: false, reason: 'Configure the app first.' };
    }

    return { ready: true };
  }

Core Concepts

Tools

Each OCP app with an Opal too function is a tool registry in Opal. Tool registry constist of one or more tools. Each tool have name, description and a list of input parameters.

Function Modes

Regular Functions (function extends ToolFunction class)

Regular functions are scoped to specific organizations and validate that requests come from the same organization:

  • Validate OptiID organization ID matches the function's organization context
  • All tools within the function are organization-scoped
  • Per-Organization Configuration: Can implement organization-specific configuration, authentication credentials, and API keys since they're tied to a single organization
  • Per-Organization Authentication: Can store and use organization-specific authentication tokens, connection strings, and other sensitive data securely

Global Functions (function extends GlobalToolFunction class)

Global functions work across all organizations without organization validation:

  • Accept OptiID authentication
  • All tools within the function are platform-wide
  • No Per-Organization Configuration: Cannot implement per-organization configuration since they work across all organizations
  • No Per-Organization Authentication: Cannot store organization-specific credentials or authentication data
  • Global Discovery: Have a global discovery URL that can be used by any organization without requiring them to install the app first

Parameters

Supported parameter types:

enum ParameterType {
  String = 'string',
  Integer = 'integer', 
  Number = 'number',
  Boolean = 'boolean',
  List = 'list',
  Dictionary = 'object'
}

Parameter Validation

The SDK automatically validates all incoming parameters against the parameter definitions you specify for your tools. When Opal sends requests to your tools, the SDK performs validation before calling your handler methods and automatically returns an error message that Opal understands. This allow Opal to auto-correct.

Automatic Validation Features

  • Type Checking: Ensures parameters match their defined types (string, integer, number, boolean, list, object)
  • Required Validation: Verifies that all required parameters are present and not null/undefined
  • Early Error Response: Returns validation errors immediately without calling your handler if validation fails
  • RFC 9457 Compliance: Error responses follow the RFC 9457 Problem Details for HTTP APIs specification

Authentication

The SDK supports authentication and authorization mechanisms:

OptiID Authentication (Default)

OptiID provides user authentication with type safety. When a tool does not declare any authRequirements, the SDK automatically adds OptiID as the default authentication provider:

interface AuthRequirementConfig {
  provider: string;        // 'OptiID'
  scopeBundle: string;     // e.g., 'calendar', 'tasks'
  required?: boolean;      // default: true
}

OptiID Token Authorization

The SDK automatically handles OptiID token validation for tool authorization. OptiID tokens provide both user authentication and authorization for tools, ensuring that only authenticated users with proper permissions can access your tools.

Token Validation:

  • The SDK extracts and validates OptiID tokens from the request body
  • Regular Tools: Validation includes verifying that requests come from the same organization as the tool
  • Global Tools: Token validation occurs but organization ID matching is skipped
  • If validation fails, returns HTTP 403 Unauthorized before reaching your handler methods
  • No additional configuration needed - validation is handled automatically
export class MyToolFunction extends ToolFunction {
  @tool({
    name: 'secure_tool',
    description: 'Tool that validates requests from Opal',
    endpoint: '/secure-endpoint',
    parameters: [
      { name: 'data', type: ParameterType.String, description: 'Data to process', required: true }
    ]
  })
  async secureToolHandler(
    params: { data: string }, 
    authData: OptiIdAuthData
  ) {
    
    // Process the request knowing it's from a trusted Opal instance
    return {
      status: 'success',
      data: `Processed: ${params.data}`,
      authorizedBy: 'Opal'
    };
  }
}

Custom Auth Providers

Tools can declare custom authentication providers instead of using the default OptiID. When a tool specifies its own authRequirements, the SDK respects that choice and skips its built-in authentication validation:

@tool({
  name: 'external_api_tool',
  description: 'Tool that uses external OAuth2 authentication',
  endpoint: '/external-api',
  parameters: [
    { name: 'query', type: ParameterType.String, description: 'Search query', required: true }
  ],
  authRequirements: [{ provider: 'google', scopeBundle: 'calendar', required: true }]
})
async externalApiTool(params: { query: string }, authData: OAuthAuthData) {
  // SDK does NOT validate oauth2 credentials
  // Your handler must validate the auth credentials
  // Validate token with your external provider
  const isValid = await this.validateOAuth2Token(authData.credentials.access_token);
  if (!isValid) {
    throw new ToolError('Invalid OAuth2 token', 401);
  }

  return { results: await this.searchExternalApi(params.query) };
}

Important Security Considerations:

Warning: Tools using non-OptiID authentication should NOT expose sensitive data from app settings (such as API keys, secrets, or organization-specific configuration).

  • OptiID auth guarantees the caller is authorized for the specific organization and the request is from a trusted Opal instance
  • Non-OptiID auth only guarantees the credentials are valid for that provider - it does NOT guarantee the caller is authorized for the organization or app

Only credentials for the declared auth provider are validated. If your tool declares google auth requirement, the SDK will pass through whatever credentials Opal sends without SDK-level validation. Your handler is fully responsible for credential validation and access control.

Error Handling

The SDK provides RFC 9457 Problem Details compliant error handling through the ToolError class. This allows you to throw errors with custom HTTP status codes and detailed error information.

ToolError Class

class ToolError extends Error {
  constructor(
    message: string,      // Error message (used as "title" in RFC 9457)
    status?: number,      // HTTP status code (default: 500)
    detail?: string       // Detailed error description (optional)
  )
}

Usage Examples

import { ToolFunction, tool, ToolError, ParameterType } from '@optimizely-opal/opal-tool-ocp-sdk';


// Throw a 404 error
throw new ToolError(
  'Task not found',
  404,
  `No task exists with ID: ${params.taskId}`
);

// Throw a 400 error for invalid input
throw new ToolError(
  'Invalid priority',
  400,
  `Priority must be one of: ${validPriorities.join(', ')}`
);

Error Response Format

When a ToolError is thrown, the SDK automatically formats it as an RFC 9457 Problem Details response:

{
  "title": "Task not found",
  "status": 404,
  "detail": "No task exists with ID: task-123",
  "instance": "/get-task"
}

Validation Errors with Multiple Fields

You can also throw ToolError with an errors array for validation scenarios with multiple field errors:

import { ToolError } from '@optimizely-opal/opal-tool-ocp-sdk';

// Validate multiple fields
const validationErrors = [];

if (!email.includes('@')) {
  validationErrors.push({
    field: 'email',
    message: 'Invalid email format'
  });
}

if (age < 0) {
  validationErrors.push({
    field: 'age',
    message: 'Age must be a positive number'
  });
}

if (validationErrors.length > 0) {
  throw new ToolError(
    'Validation failed',
    400,
    "See 'errors' field for details.",
    validationErrors
  );
}

This will return:

{
  "title": "Validation failed",
  "status": 400,
  "detail": "See 'errors' field for details.",
  "instance": "/create-user",
  "errors": [
    {
      "field": "email",
      "message": "Invalid email format"
    },
    {
      "field": "age",
      "message": "Age must be a positive number"
    }
  ]
}

Response Headers:

  • Content-Type: application/problem+json
  • HTTP status code matches the ToolError status

Note:

  • If a regular Error (not ToolError) is thrown, it will be automatically wrapped in a 500 response with RFC 9457 format
  • Parameter validation is automatic and uses this same format when validation fails

API Reference

Handler Function Signatures

All tool handler methods follow this signature pattern:

async handlerMethod(
  params: TParams,           // Tool parameters
  authData: OptiIdAuthData | OAuthAuthData  // OptiID or OAuth user authentication data
): Promise<TResult>
  • params: The input parameters for tools
  • authData: OptiID user authentication by default; can be customized in tool definition

Decorators

@tool(config: ToolConfig)

Registers a method as a discoverable tool.

interface ToolConfig {
  name: string;
  description: string;
  parameters: ParameterConfig[];
  authRequirements?: AuthRequirementConfig[];
  endpoint: string;
  tags?: string[];  // Optional tags for categorization and filtering
}

Tag-based Filtering

The SDK supports optional tags for tools to enable/disable groups of tools dynamically through settings:

@tool({
  name: 'get_user',
  description: 'Get user information',
  endpoint: '/get-user',
  parameters: [],
  tags: ['get_tool', 'default_tool_set']
})
async getUser(params: any, authData: OptiIdAuthData) {
  // Implementation
}

To enable tag-based filtering, add a tool_tags section to your settings configuration:

forms/settings.yml

sections:
  - key: tool_tags
    label: Tool Configuration
    elements:
      - type: toggle
        key: get_tool
        label: Enable GET operations
        defaultValue: true
      - type: toggle
        key: default_tool_set
        label: Enable default tool set
        defaultValue: true

Filtering behavior:

  • Tools with no tags are always enabled
  • Tools with tags are enabled only if ALL of their tags are enabled in settings
  • If any tag is disabled (set to false), the tool is excluded from both discovery and execution
  • Tags not configured in settings default to true (enabled)
  • If no tool_tags settings exist, all tools are enabled by default

Base Classes

ToolFunction

Abstract base class for organization-scoped OCP functions:

export abstract class ToolFunction extends Function {
  protected ready(): Promise<ReadyResponse>;
  public async perform(): Promise<Response>;
}

Extend this class for regular tools that validate organization IDs. The perform method automatically routes requests to registered tools and enforces organization validation.

GlobalToolFunction

Abstract base class for global OCP functions:

export abstract class GlobalToolFunction extends GlobalFunction {
  protected ready(): Promise<ReadyResponse>;
  public async perform(): Promise<Response>;
}

Extend this class for tools that work across organizations. The perform method routes requests to registered tools but skips organization validation for tools.

Models

Key model classes:

  • Tool - Represents a registered tool (auth data is OptiIdAuthData | OAuthAuthData)
  • Parameter - Defines tool parameters
  • AuthRequirement - Defines authentication needs
  • AuthData - Union type for authentication data (OptiIdAuthData | OAuthAuthData)
  • OptiIdAuthData - OptiID specific authentication data
  • OAuthAuthData - OAuth provider authentication data
  • ReadyResponse - Response type for the ready method containing status and optional reason
  • ToolError - Custom error class for RFC 9457 Problem Details error responses with configurable HTTP status codes

Discovery and Ready Endpoints

The SDK automatically provides two important endpoints:

Discovery Endpoint (/discovery)

Returns all registered tools in the proper OCP format for platform integration. The discovery endpoint returns all tools registered within the function, regardless of their individual configuration:

  • Regular Functions (ToolFunction): Returns all tools with organization-scoped behavior
  • Global Functions (GlobalToolFunction): Returns all tools with platform-wide behavior

All tools within a function operate in the same mode - there is no mixing of global and organization-scoped tools within a single function.

Example response for a regular function and global function:

{
  "functions": [
    {
      "name": "create_task",
      "description": "Creates a new task in the system",
      "parameters": [
        {
          "name": "title",
          "type": "string",
          "description": "The task title",
          "required": true
        },
        {
          "name": "priority",
          "type": "string",
          "description": "Task priority level",
          "required": false
        }
      ],
      "endpoint": "/create-task",
      "http_method": "POST"
    },
    {
      "name": "secure_task",
      "description": "Creates a secure task with OptiID authentication",
      "parameters": [
        {
          "name": "title",
          "type": "string",
          "description": "The task title",
          "required": true
        }
      ],
      "endpoint": "/secure-task",
      "http_method": "POST",
      "auth_requirements": [
        {
          "provider": "OptiID",
          "scope_bundle": "tasks",
          "required": true
        }
      ]
    }
  ]
}

Ready Endpoint (/ready)

Returns the current readiness status of your function:

{
  "ready": true
}

Or when not ready with a reason:

{
  "ready": false,
  "reason": "Missing API key"
}

This endpoint calls your function's ready() method and returns:

  • {ready: true} when the function is ready to process requests
  • {ready: false, reason?: string} when the function is not ready (missing configuration, external services unavailable, etc.), optionally with a descriptive reason
  • HTTP 200 status code regardless of ready state (the ready status is in the response body)

Development

Prerequisites

  • Node.js >= 22.0.0
  • TypeScript 5.x

Building

yarn build

Testing

# Run tests
yarn test

# Run tests in watch mode
yarn test:watch

# Run tests with coverage
yarn test:coverage

Linting

yarn lint

Examples

Function with Authentication

import { ToolFunction, tool, ParameterType, OptiIdAuthData } from '@optimizely-opal/opal-ocp-sdk';

export class AuthenticatedFunction extends ToolFunction {

  // OptiID authentication example
  @tool({
    name: 'secure_operation',
    description: 'Performs a secure operation with OptiID',
    endpoint: '/secure',
    parameters: [],
    authRequirements: [{ provider: 'OptiID', scopeBundle: 'tasks', required: true }]
  })
  async secureOperation(params: unknown, authData: OptiIdAuthData) {
    if (!authData) throw new Error('OptiID authentication required');
    
    const { customer_id, access_token } = authData.credentials;
    // Use OptiID credentials for API calls
    return { success: true, customer_id };
  }

}

Organizing Tools in Separate Files

For larger projects, you can organize your tools in separate files and import them into your main ToolFunction class:

Project Structure:

src/
├── tools/
│   ├── index.ts
│   ├── TaskTool.ts
│   └── NotificationTool.ts
├── MyToolFunction.ts
└── MyGlobalToolFunction.ts

tools/TaskTool.ts:

import { tool, ParameterType, OptiIdAuthData } from '@optimizely-opal/opal-ocp-sdk';

export class TaskTool {
  @tool({
    name: 'create_task',
    description: 'Creates a new task in the system',
    endpoint: '/create-task',
    parameters: [
      {
        name: 'title',
        type: ParameterType.String,
        description: 'The task title',
        required: true
      },
      {
        name: 'priority',
        type: ParameterType.String,
        description: 'Task priority level',
        required: false
      }
    ]
  })
  async createTask(params: { title: string; priority?: string }, authData: OptiIdAuthData) {
    return {
      id: '123',
      title: params.title,
      priority: params.priority || 'medium'
    };
  }

  @tool({
    name: 'delete_task',
    description: 'Deletes a task from the system',
    endpoint: '/delete-task',
    parameters: [
      {
        name: 'taskId',
        type: ParameterType.String,
        description: 'The task ID to delete',
        required: true
      }
    ]
  })
  async deleteTask(params: { taskId: string }, authData: OptiIdAuthData) {
    return { success: true, deletedTaskId: params.taskId };
  }
}

tools/NotificationTool.ts:

import { tool, ParameterType, OptiIdAuthData } from '@optimizely-opal/opal-ocp-sdk';

export class NotificationTool {
  @tool({
    name: 'send_notification',
    description: 'Sends a notification to users',
    endpoint: '/send-notification',
    parameters: [
      {
        name: 'message',
        type: ParameterType.String,
        description: 'The notification message',
        required: true
      },
      {
        name: 'userId',
        type: ParameterType.String,
        description: 'Target user ID',
        required: true
      }
    ]
  })
  async sendNotification(params: { message: string; userId: string }, authData: OptiIdAuthData) {
    return {
      notificationId: '456',
      message: params.message,
      userId: params.userId,
      sent: true
    };
  }

}

tools/index.ts:

export * from './TaskTool';
export * from './NotificationTool';

MyToolFunction.ts:

import { ToolFunction } from '@optimizely-opal/opal-ocp-sdk';
import * from './tools';

export class MyToolFunction extends ToolFunction {
}

This approach provides several benefits:

  • Better organization: Each tool has its own file with related methods
  • Maintainability: Easier to find and modify specific tools
  • Reusability: Tools can be shared across different ToolFunction classes
  • Team collaboration: Different developers can work on different tool files
  • Testing: Each tool class can be unit tested independently

Decorator Behavior and Instance Context

The @tool decorator provide intelligent instance context management that behaves differently depending on where the decorated methods are defined:

ToolFunction Subclass Context

When decorators are used in a class that extends ToolFunction, the decorators can reuse the existing ToolFunction instance when called through the perform() method:

export class MyToolFunction extends ToolFunction {
  private secretKey = process.env.SECRET_KEY;

  @tool({
    name: 'process_data',
    description: 'Processes data using instance context',
    endpoint: '/process-data',
    parameters: [
      { name: 'data', type: ParameterType.String, description: 'Data to process', required: true }
    ]
  })
  async processData(params: { data: string }) {
    // ✅ Can access instance properties and methods
    // ✅ Can access this.request (inherited from ToolFunction)
    // ✅ Shares state with other methods in the same request

    const userAgent = this.request.headers.get('user-agent');
    return {
      processedData: this.encryptData(params.data),
      userAgent,
      timestamp: Date.now()
    };
  }

  private encryptData(data: string): string {
    // Uses instance property
    return `encrypted_${data}_${this.secretKey}`;
  }
}

Standalone Class Context

When decorators are used in classes that don't extend ToolFunction, the decorators create new instances for each handler call:

export class StandaloneToolService {
  private config = { apiKey: 'standalone-key' };

  @tool({
    name: 'standalone_operation',
    description: 'Standalone operation without ToolFunction',
    endpoint: '/standalone',
    parameters: [
      { name: 'input', type: ParameterType.String, description: 'Input data', required: true }
    ]
  })
  async standaloneOperation(params: { input: string }) {
    // ✅ Can access instance properties and methods
    // ❌ Cannot access this.request (not inherited from ToolFunction)
    // ❌ No shared state with ToolFunction lifecycle

    return {
      result: `${this.helperMethod()}: ${params.input}`,
      source: 'standalone'
    };
  }

  private helperMethod() {
    return this.config.apiKey;
  }
}

This behavior ensures that your tools can be both flexible (working in any class) and powerful (leveraging ToolFunction features when available).