OCP SDK for Opal tool
npm install @optimizely-opal/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.
- ๐ฏ 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
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:
``bash`
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
`yaml`
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. ToolFunction
Function class must extend either or GlobalToolFunction class from @optimizely-opal/opal-tool-ocp-sdk (See 'Function modes' section below).
src/functions/OpalToolFunction
`typescript
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
`typescript
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. ready
OCP will call this method to show tool readiness in the UI. Only functions marked as can be registered and called by Opal.reason
The value of property from the response will be displayed in OCP UI to inform users why the tool is not ready to be registered. `typescript
protected override async ready(): Promise
// validation logic
if (!isValid) {
return { ready: false, reason: 'Configure the app first.' };
}
return { ready: true };
}
`
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.
#### 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
Supported parameter types:
`typescript`
enum ParameterType {
String = 'string',
Integer = 'integer',
Number = 'number',
Boolean = 'boolean',
List = 'list',
Dictionary = 'object'
}
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
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:
`typescript`
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
`typescriptProcessed: ${params.data}
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: ,`
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:
`typescript
@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.
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.
`typescript`
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)
)
}
`typescript
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(', ')}`
);
When a ToolError is thrown, the SDK automatically formats it as an RFC 9457 Problem Details response:
`json`
{
"title": "Task not found",
"status": 404,
"detail": "No task exists with ID: task-123",
"instance": "/get-task"
}
You can also throw ToolError with an errors array for validation scenarios with multiple field errors:
`typescript
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:
`json`
{
"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+jsonToolError
- HTTP status code matches the 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
All tool handler methods follow this signature pattern:
`typescript`
async handlerMethod(
params: TParams, // Tool parameters
authData: OptiIdAuthData | OAuthAuthData // OptiID or OAuth user authentication data
): Promise
- params: The input parameters for tools
- authData: OptiID user authentication by default; can be customized in tool definition
#### @tool(config: ToolConfig)
Registers a method as a discoverable tool.
`typescript`
interface ToolConfig {
name: string;
description: string;
parameters: ParameterConfig[];
authRequirements?: AuthRequirementConfig[];
endpoint: string;
}
#### ToolFunction
Abstract base class for organization-scoped OCP functions:
`typescript`
export abstract class ToolFunction extends Function {
protected ready(): Promise
public async perform(): Promise
}
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:
`typescript`
export abstract class GlobalToolFunction extends GlobalFunction {
protected ready(): Promise
public async perform(): Promise
}
Extend this class for tools that work across organizations. The perform method routes requests to registered tools but skips organization validation for tools.
Key model classes:
- Tool - Represents a registered tool (auth data is OptiIdAuthData | 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
The SDK automatically provides two important endpoints:
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 behaviorGlobalToolFunction
- Global Functions (): 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:
`json`
{
"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
}
]
}
]
}
Returns the current readiness status of your function:
`json`
{
"ready": true
}
Or when not ready with a reason:
`json`
{
"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)
- Node.js >= 22.0.0
- TypeScript 5.x
`bash`
yarn build
`bashRun tests
yarn test
$3
`bash
yarn lint
`Examples
$3
`typescript
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 };
}
}
`$3
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:
`typescript
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:
`typescript
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:
`typescript
export * from './TaskTool';
export * from './NotificationTool';
`MyToolFunction.ts:
`typescript
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:$3
When decorators are used in a class that extends
ToolFunction, the decorators can reuse the existing ToolFunction instance when called through the perform() method:`typescript
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};
}
}
`$3
When decorators are used in classes that don't extend
ToolFunction, the decorators create new instances for each handler call:`typescript
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).