@optimizely-opal/opal-tool-ocp-sdk
v1.1.6
Published
OCP SDK for Opal tool
Keywords
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
@tooldecorator 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
ToolFunctionorGlobalToolFunctionfor 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-sdkTo 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: trueNext, 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
/discoveryendpoint 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
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
ToolErrorstatus
Note:
- If a regular
Error(notToolError) 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: trueFiltering 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_tagssettings 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 isOptiIdAuthData | OAuthAuthData)Parameter- Defines tool parametersAuthRequirement- Defines authentication needsAuthData- Union type for authentication data (OptiIdAuthData | OAuthAuthData)OptiIdAuthData- OptiID specific authentication dataOAuthAuthData- OAuth provider authentication dataReadyResponse- Response type for the ready method containing status and optional reasonToolError- 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 buildTesting
# Run tests
yarn test
# Run tests in watch mode
yarn test:watch
# Run tests with coverage
yarn test:coverageLinting
yarn lintExamples
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.tstools/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).
