@optimizely-opal/opal-tools-sdk
v0.1.32-dev
Published
SDK for creating Opal-compatible tools services
Readme
Opal Tools SDK for TypeScript
This SDK simplifies the creation of tools services compatible with the Opal Tools Management Service.
Features
- Easy definition of tool functions with decorators
- Automatic generation of discovery endpoints
- Parameter validation and type checking
- Authentication helpers
- Express integration
- Island components for interactive UI responses
Installation
npm install @optimizely-opal/opal-tools-sdkUsage
import {
ToolsService,
tool,
IslandResponse,
IslandConfig,
} from "@optimizely-opal/opal-tools-sdk";
import express from "express";
const app = express();
const toolsService = new ToolsService(app);
interface WeatherParameters {
location: string;
units: string;
}
class WeatherTool {
@tool({
name: "get_weather",
description: "Gets current weather for a location",
})
async getWeather(parameters: WeatherParameters) {
// Implementation...
return { temperature: 22, condition: "sunny" };
}
}
// Discovery endpoint is automatically created at /discoveryAuthentication
The SDK provides two ways to require authentication for your tools:
1. Using the @requiresAuth decorator
import { ToolsService, tool } from "@optimizely-opal/opal-tools-sdk";
import { requiresAuth } from "@optimizely-opal/opal-tools-sdk/auth";
import express from "express";
const app = express();
const toolsService = new ToolsService(app);
interface CalendarParameters {
date: string;
timezone?: string;
}
class CalendarTool {
// Single authentication requirement
@requiresAuth({ provider: "google", scopeBundle: "calendar", required: true })
@tool({
name: "get_calendar_events",
description: "Gets calendar events for a date",
})
async getCalendarEvents(parameters: CalendarParameters, authData?: any) {
// The authData parameter contains authentication information
const token = authData?.credentials?.access_token || "";
// Use the token to make authenticated requests
// ...
return { events: ["Meeting at 10:00", "Lunch at 12:00"] };
}
// Multiple authentication requirements (tool can work with either provider)
@requiresAuth({ provider: "google", scopeBundle: "calendar", required: true })
@requiresAuth({
provider: "microsoft",
scopeBundle: "outlook",
required: true,
})
@tool({
name: "get_calendar_availability",
description: "Check calendar availability",
})
async getCalendarAvailability(
parameters: CalendarParameters,
authData?: any,
) {
const provider = authData?.provider || "";
const token = authData?.credentials?.access_token || "";
if (provider === "google") {
// Use Google Calendar API
} else if (provider === "microsoft") {
// Use Microsoft Outlook API
}
return { available: true, provider_used: provider };
}
}2. Specifying auth requirements in the @tool decorator
interface EmailParameters {
limit?: number;
folder?: string;
}
class EmailTool {
@tool({
name: "get_email",
description: "Gets emails from the user's inbox",
authRequirements: {
provider: "google",
scopeBundle: "gmail",
required: true,
},
})
async getEmail(parameters: EmailParameters, authData?: any) {
// Implementation...
return { emails: ["Email 1", "Email 2"] };
}
}Authentication
The SDK provides authentication support for tools that require user credentials:
import {
ToolsService,
tool,
requiresAuth,
AuthData,
} from "@optimizely-opal/opal-tools-sdk";
import express from "express";
interface CalendarParameters {
date: string;
timezone?: string;
}
const app = express();
const toolsService = new ToolsService(app);
class CalendarTool {
@requiresAuth({ provider: "google", scopeBundle: "calendar", required: true })
@tool({
name: "get_calendar_events",
description: "Gets calendar events for a date",
})
async getCalendarEvents(parameters: CalendarParameters, authData?: AuthData) {
// The authData parameter contains authentication information
if (authData) {
const token = authData.credentials.access_token;
// Use the token to make authenticated requests
}
return { events: ["Meeting at 10:00", "Lunch at 12:00"] };
}
}Interactions
Interactions are app-only handlers — actions callable from the UI but hidden from the LLM. Use them when a tool surface needs to expose a button, form submit, or other follow-up action that the model should not be able to call. They follow the MCP "app-only tools" pattern (visibility: ["app"]) and are not listed in the /discovery endpoint.
When to use registerInteraction vs @tool / registerTool:
- Use
@tool/registerToolwhen the LLM should be able to call the function on its own. - Use
registerInteractionwhen only the UI (action cards, islands) should call it — the model should never see it.
Declaring an interaction
import express from "express";
import { z } from "zod/v4";
import {
ToolsService,
registerInteraction,
type InteractionContext,
} from "@optimizely-opal/opal-tools-sdk";
const app = express();
new ToolsService(app);
registerInteraction(
"submit_task_form",
{
description: "Handle task form submission",
inputSchema: {
title: z.string().describe("Task title"),
priority: z.string().default("medium").describe("Task priority"),
assignee: z.string().optional().describe("Assignee email"),
},
},
async (parameters, context: InteractionContext) => {
// parameters is typed as { title: string; priority: string; assignee?: string }
// context.auth_data carries credentials when auth is configured
return {
task_id: "task-123",
message: `Task '${parameters.title}' created`,
};
},
);The Zod schema is converted to the same Parameter[] representation registerTool uses, and the handler's parameters argument is statically typed from it.
Handler signature & InteractionContext
InteractionContext carries the auth data resolved by TMS from the parent tool's auth requirements:
type InteractionContext = {
auth_data?: AuthData;
};If the parent tool did not declare auth requirements (or no credentials are available), context.auth_data is undefined.
Generated endpoint
The SDK exposes a single shared endpoint for all interactions:
POST /interactions/execute
Content-Type: application/json
{
"name": "submit_task_form",
"parameters": {"title": "Ship docs", "priority": "high"},
"auth": {"provider": "...", "credentials": {...}}
}The response is whatever the handler returns, serialized as JSON. Interactions are not listed in /discovery.
Name uniqueness
Interaction names must be unique across both tools and interactions in the same service, since both can be exported as MCP tools. Registering an interaction with a name that conflicts with an existing tool or interaction throws an error.
Island Components
The SDK includes Island components for creating interactive UI responses:
import {
ToolsService,
tool,
IslandResponse,
IslandConfig,
} from "@optimizely-opal/opal-tools-sdk";
import express from "express";
interface WeatherParameters {
location: string;
units?: string;
}
const app = express();
const toolsService = new ToolsService(app);
class WeatherTool {
@tool({
name: "get_weather",
description: "Gets current weather for a location",
})
async getWeather(parameters: WeatherParameters) {
// Get weather data (implementation details omitted)
const weatherData = { temperature: 22, condition: "sunny", humidity: 65 };
// Create an interactive island for weather settings
const island = new IslandConfig(
[
new IslandConfig.Field(
"location",
"Location",
"string",
parameters.location,
),
new IslandConfig.Field(
"units",
"Temperature Units",
"string",
parameters.units || "metric",
false,
["metric", "imperial", "kelvin"],
),
new IslandConfig.Field(
"current_temp",
"Current Temperature",
"string",
`${weatherData.temperature}°${parameters.units === "metric" ? "C" : "F"}`,
),
],
[
new IslandConfig.Action(
"refresh_weather",
"Refresh Weather",
"button",
"/tools/get_weather",
"update",
),
],
"button", // Optional island type
"plus", // Optional island icon
);
return IslandResponse.create([island]);
}
}Island Components
IslandConfig.Field
Fields represent data inputs in the UI:
name: Programmatic field identifierlabel: Human-readable labeltype: Field type ('string','boolean','json')value: Current field value (optional)hidden: Whether to hide from user (optional, default: false)options: Available options for selection (optional)
IslandConfig.Action
Actions represent buttons or operations:
name: Programmatic action identifierlabel: Human-readable button labeltype: UI element type (typically'button')endpoint: API endpoint to calloperation: Operation type (default:'create')
IslandConfig
Contains the complete island configuration:
fields: Array of IslandConfig.Field objectsactions: Array of IslandConfig.Action objectstype: Optional island type for custom rendering (optional)icon: Optional icon identifier for the island (optional)
IslandResponse
The response wrapper for islands:
- Use
IslandResponse.create([islands])to create responses - Supports multiple islands per response
Resources & Proteus UI
The SDK supports defining MCP resources that serve dynamic UI specifications using the Proteus framework. This enables tools to render rich, interactive interfaces without hardcoded frontend integrations.
For the full Proteus component reference and visual designer, see the Proteus documentation.
Registering a Resource with registerResource
Use registerResource to define an MCP resource:
import { registerResource, UI } from "@optimizely-opal/opal-tools-sdk";
const getCreateForm = registerResource(
"create-form",
{
uri: "ui://my-app/create-form",
description: "Form for creating new items",
title: "Create Form",
},
async () => {
return UI.Document({
title: "Create Item",
body: [
UI.Heading({ children: "New Item" }),
UI.Field({
label: "Item Name",
children: UI.Input({
name: "item_name",
placeholder: "Enter item name",
}),
}),
UI.Field({
label: "Description",
children: UI.Textarea({
name: "description",
placeholder: "Enter description",
}),
}),
],
actions: [
UI.Action({ children: "Save", appearance: "primary" }),
UI.CancelAction({ children: "Cancel" }),
],
});
},
);Parameters:
name(required): Name of the resourceoptions.uri(required): Unique URI for the resource (e.g.,'ui://my-app/create-form')options.description(optional): Description of the resourceoptions.mimeType(optional): MIME type. Auto-set to'application/vnd.opal.proteus+json'when returning aUI.Documentoptions.title(optional): Human-readable titlehandler: Async function returning astringorUI.Document
Linking a Tool to a UI Resource
Use the uiResource option on registerTool to associate a tool with a Proteus UI resource:
import { registerTool } from "@optimizely-opal/opal-tools-sdk";
registerTool(
"create_item",
{
description: "Create a new item",
uiResource: "ui://my-app/create-form",
},
async (parameters: CreateItemParams) => {
return { id: "item-123", name: parameters.name, status: "created" };
},
);Or with the @tool decorator on a class method:
class ItemTool {
@tool({
name: "create_item",
description: "Create a new item",
uiResource: "ui://my-app/create-form",
})
async createItem(parameters: CreateItemParams) {
return { id: "item-123", name: parameters.name, status: "created" };
}
}Building UI with UI.Document
Import the UI namespace from the SDK. It provides factory functions for all Proteus components:
import { UI } from "@optimizely-opal/opal-tools-sdk";Available components:
| Category | Components |
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| Layout | UI.Document(), UI.Group(), UI.Card(), UI.CardHeader(), UI.CardLink(), UI.Separator() |
| Typography | UI.Heading(), UI.Text(), UI.Link() |
| Data Display | UI.Avatar(), UI.Badge(), UI.DataTable(), UI.Chart(), UI.IconCalendar(), UI.Image(), UI.ImageCarousel(), UI.Time() |
| Form Controls | UI.Field(), UI.Input(), UI.Textarea(), UI.Select(), UI.SelectTrigger(), UI.SelectContent(), UI.Switch(), UI.Range(), UI.Question() |
| Actions | UI.Action(), UI.CancelAction() |
| Dynamic | UI.Value(), UI.Map(), UI.MapIndex(), UI.Show(), UI.Concat(), UI.Zip() |
Data binding with UI.Value resolves paths from the tool response:
const getResults = registerResource(
"results",
{
uri: "ui://my-app/results",
},
async () => {
return UI.Document({
title: UI.Value({ path: "/title" }),
body: UI.Map({
path: "/items",
children: UI.Text({ children: UI.Value({ path: "name" }) }),
}),
});
},
);Conditional rendering with UI.Show:
UI.Show({
when: { "!!": UI.Value({ path: "/error" }) },
children: UI.Text({ children: "An error occurred", color: "fg.error" }),
});The MIME type constant is available as UI.MIME_TYPE ('application/vnd.opal.proteus+json').
Type Definitions
The SDK provides comprehensive TypeScript type definitions:
Authentication Types
AuthData: Interface containing provider and credentials informationCredentials: Interface with access_token, org_sso_id, customer_id, instance_id, and product_skuEnvironment: Interface specifying execution mode ('headless'or'interactive')
Parameter Types
ParameterType: Enum for supported parameter typesParameter: Class for tool parameter definitionsFunction: Class for complete tool function definitions
These types provide full IDE support and compile-time type checking for better development experience.
Documentation
See full documentation for more examples and configuration options.
