@optimizely-opal/opal-tools-sdk 0.1.0-dev

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,48 @@
1
+ # Opal Tools SDK for TypeScript
2
+
3
+ This SDK simplifies the creation of tools services compatible with the Opal Tools Management Service.
4
+
5
+ ## Features
6
+
7
+ - Type definitions for tools and parameters
8
+ - Express middleware for discovery endpoints
9
+ - Decorators for tool registration
10
+ - Parameter validation
11
+ - Authentication helpers
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install @optimizely-opal/opal-tools-sdk
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ```typescript
22
+ import { ToolsService, tool } from '@optimizely-opal/opal-tools-sdk';
23
+ import express from 'express';
24
+
25
+ interface WeatherParameters {
26
+ location: string;
27
+ units?: string;
28
+ }
29
+
30
+ const app = express();
31
+ const toolsService = new ToolsService(app);
32
+
33
+ @tool({
34
+ name: 'get_weather',
35
+ description: 'Gets current weather for a location'
36
+ })
37
+ async function getWeather(parameters: WeatherParameters) {
38
+ // Implementation...
39
+ return { temperature: 22, condition: 'sunny' };
40
+ }
41
+
42
+ // Discovery endpoint is automatically created at /discovery
43
+ app.listen(3000);
44
+ ```
45
+
46
+ ## Documentation
47
+
48
+ See full documentation for more examples and configuration options.
package/dist/auth.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Middleware to handle authentication requirements
3
+ * @param req Express request
4
+ * @param res Express response
5
+ * @param next Express next function
6
+ */
7
+ export declare function authMiddleware(req: any, res: any, next: any): any;
8
+ interface AuthOptions {
9
+ provider: string;
10
+ scopeBundle: string;
11
+ required?: boolean;
12
+ }
13
+ /**
14
+ * Decorator to indicate that a tool requires authentication
15
+ * @param options Authentication options
16
+ */
17
+ export declare function requiresAuth(options: AuthOptions): (target: any, propertyKey?: string, descriptor?: PropertyDescriptor) => any;
18
+ export {};
package/dist/auth.js ADDED
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.authMiddleware = authMiddleware;
4
+ exports.requiresAuth = requiresAuth;
5
+ /**
6
+ * Middleware to handle authentication requirements
7
+ * @param req Express request
8
+ * @param res Express response
9
+ * @param next Express next function
10
+ */
11
+ function authMiddleware(req, res, next) {
12
+ const authHeader = req.headers.authorization;
13
+ if (!authHeader && req.authRequired) {
14
+ return res.status(401).json({ error: 'Authentication required' });
15
+ }
16
+ // The Tools Management Service will provide the appropriate token
17
+ // in the Authorization header
18
+ next();
19
+ }
20
+ /**
21
+ * Decorator to indicate that a tool requires authentication
22
+ * @param options Authentication options
23
+ */
24
+ function requiresAuth(options) {
25
+ return function (target, propertyKey, descriptor) {
26
+ const isMethod = propertyKey && descriptor;
27
+ if (isMethod && descriptor) {
28
+ const originalMethod = descriptor.value;
29
+ descriptor.value = function (...args) {
30
+ // Store auth requirements in function metadata
31
+ const fn = originalMethod;
32
+ fn.__authRequirements__ = {
33
+ provider: options.provider,
34
+ scopeBundle: options.scopeBundle,
35
+ required: options.required ?? true
36
+ };
37
+ return originalMethod.apply(this, args);
38
+ };
39
+ return descriptor;
40
+ }
41
+ return target;
42
+ };
43
+ }
@@ -0,0 +1,36 @@
1
+ import 'reflect-metadata';
2
+ import { ParameterType } from './models';
3
+ interface ParameterDefinition {
4
+ name: string;
5
+ type: ParameterType;
6
+ description: string;
7
+ required: boolean;
8
+ }
9
+ interface ToolOptions {
10
+ name: string;
11
+ description: string;
12
+ parameters?: ParameterDefinition[];
13
+ authRequirements?: {
14
+ provider: string;
15
+ scopeBundle: string;
16
+ required?: boolean;
17
+ };
18
+ }
19
+ /**
20
+ * Decorator to register a function as an Opal tool
21
+ * @param options Tool options including:
22
+ * - name: Name of the tool
23
+ * - description: Description of the tool
24
+ * - authRequirements: (Optional) Authentication requirements
25
+ * Format: { provider: "oauth_provider", scopeBundle: "permissions_scope", required: true }
26
+ * Example: { provider: "google", scopeBundle: "calendar", required: true }
27
+ *
28
+ * Note: If your tool requires authentication, define your handler function with two parameters:
29
+ * ```
30
+ * async function myTool(parameters: ParameterInterface, authData?: any): Promise<any> {
31
+ * // Your tool implementation
32
+ * }
33
+ * ```
34
+ */
35
+ export declare function tool(options: ToolOptions): (target: any, propertyKey?: string, descriptor?: PropertyDescriptor) => any;
36
+ export {};
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.tool = tool;
4
+ require("reflect-metadata");
5
+ const models_1 = require("./models");
6
+ const registry_1 = require("./registry");
7
+ /**
8
+ * Map a TypeScript type to a ParameterType
9
+ * @param type TypeScript type
10
+ */
11
+ function mapTypeToParameterType(type) {
12
+ if (type === String || type.name === 'String') {
13
+ return models_1.ParameterType.String;
14
+ }
15
+ else if (type === Number || type.name === 'Number') {
16
+ return models_1.ParameterType.Number;
17
+ }
18
+ else if (type === Boolean || type.name === 'Boolean') {
19
+ return models_1.ParameterType.Boolean;
20
+ }
21
+ else if (type === Array || type.name === 'Array') {
22
+ return models_1.ParameterType.List;
23
+ }
24
+ else if (type === Object || type.name === 'Object') {
25
+ return models_1.ParameterType.Dictionary;
26
+ }
27
+ // Default to string
28
+ return models_1.ParameterType.String;
29
+ }
30
+ /**
31
+ * Extract parameters from a TypeScript interface
32
+ * @param paramType Parameter type object
33
+ */
34
+ function extractParameters(paramType) {
35
+ const parameters = [];
36
+ // This is very basic and doesn't handle complex types
37
+ // For production use, this would need to be more sophisticated
38
+ for (const key in paramType) {
39
+ if (paramType.hasOwnProperty(key)) {
40
+ const type = typeof paramType[key] === 'undefined' ? String : paramType[key].constructor;
41
+ const required = true; // In a real implementation, we'd detect optional parameters
42
+ parameters.push(new models_1.Parameter(key, mapTypeToParameterType(type), '', // Description - in a real impl we'd use TypeDoc or similar
43
+ required));
44
+ }
45
+ }
46
+ return parameters;
47
+ }
48
+ /**
49
+ * Decorator to register a function as an Opal tool
50
+ * @param options Tool options including:
51
+ * - name: Name of the tool
52
+ * - description: Description of the tool
53
+ * - authRequirements: (Optional) Authentication requirements
54
+ * Format: { provider: "oauth_provider", scopeBundle: "permissions_scope", required: true }
55
+ * Example: { provider: "google", scopeBundle: "calendar", required: true }
56
+ *
57
+ * Note: If your tool requires authentication, define your handler function with two parameters:
58
+ * ```
59
+ * async function myTool(parameters: ParameterInterface, authData?: any): Promise<any> {
60
+ * // Your tool implementation
61
+ * }
62
+ * ```
63
+ */
64
+ function tool(options) {
65
+ return function (target, propertyKey, descriptor) {
66
+ const isMethod = propertyKey && descriptor;
67
+ const handler = isMethod ? descriptor.value : target;
68
+ // Generate endpoint from name - ensure hyphens instead of underscores
69
+ const endpoint = `/tools/${options.name.replace(/_/g, '-')}`;
70
+ // Convert parameter definitions to Parameter objects
71
+ const parameters = [];
72
+ if (options.parameters && options.parameters.length > 0) {
73
+ // Use the explicitly provided parameter definitions
74
+ for (const paramDef of options.parameters) {
75
+ parameters.push(new models_1.Parameter(paramDef.name, paramDef.type, paramDef.description, paramDef.required));
76
+ }
77
+ }
78
+ // Create auth requirements if specified
79
+ let authRequirements;
80
+ if (options.authRequirements) {
81
+ authRequirements = [
82
+ new models_1.AuthRequirement(options.authRequirements.provider, options.authRequirements.scopeBundle, options.authRequirements.required ?? true)
83
+ ];
84
+ }
85
+ // Register the tool with all services
86
+ for (const service of registry_1.registry.services) {
87
+ service.registerTool(options.name, options.description, handler, parameters, endpoint, authRequirements);
88
+ }
89
+ return isMethod ? descriptor : target;
90
+ };
91
+ }
@@ -0,0 +1,5 @@
1
+ import 'reflect-metadata';
2
+ export { ToolsService } from './service';
3
+ export { tool } from './decorators';
4
+ export { requiresAuth } from './auth';
5
+ export * from './models';
package/dist/index.js ADDED
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.requiresAuth = exports.tool = exports.ToolsService = void 0;
18
+ require("reflect-metadata");
19
+ var service_1 = require("./service");
20
+ Object.defineProperty(exports, "ToolsService", { enumerable: true, get: function () { return service_1.ToolsService; } });
21
+ var decorators_1 = require("./decorators");
22
+ Object.defineProperty(exports, "tool", { enumerable: true, get: function () { return decorators_1.tool; } });
23
+ var auth_1 = require("./auth");
24
+ Object.defineProperty(exports, "requiresAuth", { enumerable: true, get: function () { return auth_1.requiresAuth; } });
25
+ __exportStar(require("./models"), exports);
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Types of parameters supported by Opal tools
3
+ */
4
+ export declare enum ParameterType {
5
+ String = "string",
6
+ Integer = "integer",
7
+ Number = "number",
8
+ Boolean = "boolean",
9
+ List = "list",
10
+ Dictionary = "object"
11
+ }
12
+ /**
13
+ * Parameter definition for an Opal tool
14
+ */
15
+ export declare class Parameter {
16
+ name: string;
17
+ type: ParameterType;
18
+ description: string;
19
+ required: boolean;
20
+ /**
21
+ * Create a new parameter definition
22
+ * @param name Parameter name
23
+ * @param type Parameter type
24
+ * @param description Parameter description
25
+ * @param required Whether the parameter is required
26
+ */
27
+ constructor(name: string, type: ParameterType, description: string, required: boolean);
28
+ /**
29
+ * Convert to JSON for the discovery endpoint
30
+ */
31
+ toJSON(): {
32
+ name: string;
33
+ type: ParameterType;
34
+ description: string;
35
+ required: boolean;
36
+ };
37
+ }
38
+ /**
39
+ * Authentication requirements for an Opal tool
40
+ */
41
+ export declare class AuthRequirement {
42
+ provider: string;
43
+ scopeBundle: string;
44
+ required: boolean;
45
+ /**
46
+ * Create a new authentication requirement
47
+ * @param provider Auth provider (e.g., "google", "microsoft")
48
+ * @param scopeBundle Scope bundle (e.g., "calendar", "drive")
49
+ * @param required Whether authentication is required
50
+ */
51
+ constructor(provider: string, scopeBundle: string, required?: boolean);
52
+ /**
53
+ * Convert to JSON for the discovery endpoint
54
+ */
55
+ toJSON(): {
56
+ provider: string;
57
+ scope_bundle: string;
58
+ required: boolean;
59
+ };
60
+ }
61
+ /**
62
+ * Function definition for an Opal tool
63
+ */
64
+ export declare class Function {
65
+ name: string;
66
+ description: string;
67
+ parameters: Parameter[];
68
+ endpoint: string;
69
+ authRequirements?: AuthRequirement[] | undefined;
70
+ /**
71
+ * HTTP method for the endpoint (default: POST)
72
+ */
73
+ httpMethod: string;
74
+ /**
75
+ * Create a new function definition
76
+ * @param name Function name
77
+ * @param description Function description
78
+ * @param parameters Function parameters
79
+ * @param endpoint API endpoint
80
+ * @param authRequirements Authentication requirements (optional)
81
+ */
82
+ constructor(name: string, description: string, parameters: Parameter[], endpoint: string, authRequirements?: AuthRequirement[] | undefined);
83
+ /**
84
+ * Convert to JSON for the discovery endpoint
85
+ */
86
+ toJSON(): any;
87
+ }
package/dist/models.js ADDED
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Function = exports.AuthRequirement = exports.Parameter = exports.ParameterType = void 0;
4
+ /**
5
+ * Types of parameters supported by Opal tools
6
+ */
7
+ var ParameterType;
8
+ (function (ParameterType) {
9
+ ParameterType["String"] = "string";
10
+ ParameterType["Integer"] = "integer";
11
+ ParameterType["Number"] = "number";
12
+ ParameterType["Boolean"] = "boolean";
13
+ ParameterType["List"] = "list";
14
+ ParameterType["Dictionary"] = "object";
15
+ })(ParameterType || (exports.ParameterType = ParameterType = {}));
16
+ /**
17
+ * Parameter definition for an Opal tool
18
+ */
19
+ class Parameter {
20
+ /**
21
+ * Create a new parameter definition
22
+ * @param name Parameter name
23
+ * @param type Parameter type
24
+ * @param description Parameter description
25
+ * @param required Whether the parameter is required
26
+ */
27
+ constructor(name, type, description, required) {
28
+ this.name = name;
29
+ this.type = type;
30
+ this.description = description;
31
+ this.required = required;
32
+ }
33
+ /**
34
+ * Convert to JSON for the discovery endpoint
35
+ */
36
+ toJSON() {
37
+ return {
38
+ name: this.name,
39
+ type: this.type,
40
+ description: this.description,
41
+ required: this.required
42
+ };
43
+ }
44
+ }
45
+ exports.Parameter = Parameter;
46
+ /**
47
+ * Authentication requirements for an Opal tool
48
+ */
49
+ class AuthRequirement {
50
+ /**
51
+ * Create a new authentication requirement
52
+ * @param provider Auth provider (e.g., "google", "microsoft")
53
+ * @param scopeBundle Scope bundle (e.g., "calendar", "drive")
54
+ * @param required Whether authentication is required
55
+ */
56
+ constructor(provider, scopeBundle, required = true) {
57
+ this.provider = provider;
58
+ this.scopeBundle = scopeBundle;
59
+ this.required = required;
60
+ }
61
+ /**
62
+ * Convert to JSON for the discovery endpoint
63
+ */
64
+ toJSON() {
65
+ return {
66
+ provider: this.provider,
67
+ scope_bundle: this.scopeBundle,
68
+ required: this.required
69
+ };
70
+ }
71
+ }
72
+ exports.AuthRequirement = AuthRequirement;
73
+ /**
74
+ * Function definition for an Opal tool
75
+ */
76
+ class Function {
77
+ /**
78
+ * Create a new function definition
79
+ * @param name Function name
80
+ * @param description Function description
81
+ * @param parameters Function parameters
82
+ * @param endpoint API endpoint
83
+ * @param authRequirements Authentication requirements (optional)
84
+ */
85
+ constructor(name, description, parameters, endpoint, authRequirements) {
86
+ this.name = name;
87
+ this.description = description;
88
+ this.parameters = parameters;
89
+ this.endpoint = endpoint;
90
+ this.authRequirements = authRequirements;
91
+ /**
92
+ * HTTP method for the endpoint (default: POST)
93
+ */
94
+ this.httpMethod = 'POST';
95
+ }
96
+ /**
97
+ * Convert to JSON for the discovery endpoint
98
+ */
99
+ toJSON() {
100
+ const result = {
101
+ name: this.name,
102
+ description: this.description,
103
+ parameters: this.parameters.map(p => p.toJSON()),
104
+ endpoint: this.endpoint,
105
+ http_method: this.httpMethod
106
+ };
107
+ if (this.authRequirements && this.authRequirements.length > 0) {
108
+ result.auth_requirements = this.authRequirements.map(auth => auth.toJSON());
109
+ }
110
+ return result;
111
+ }
112
+ }
113
+ exports.Function = Function;
@@ -0,0 +1,7 @@
1
+ import { ToolsService } from './service';
2
+ /**
3
+ * Internal registry for ToolsService instances
4
+ */
5
+ export declare const registry: {
6
+ services: ToolsService[];
7
+ };
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registry = void 0;
4
+ /**
5
+ * Internal registry for ToolsService instances
6
+ */
7
+ exports.registry = {
8
+ services: []
9
+ };
@@ -0,0 +1,27 @@
1
+ import { Express } from 'express';
2
+ import { AuthRequirement, Parameter } from './models';
3
+ export declare class ToolsService {
4
+ private app;
5
+ private router;
6
+ private functions;
7
+ /**
8
+ * Initialize a new tools service
9
+ * @param app Express application
10
+ */
11
+ constructor(app: Express);
12
+ /**
13
+ * Initialize the discovery endpoint
14
+ */
15
+ private initRoutes;
16
+ /**
17
+ * Register a tool function
18
+ * @param name Tool name
19
+ * @param description Tool description
20
+ * @param handler Function implementing the tool
21
+ * @param parameters List of parameters for the tool
22
+ * @param endpoint API endpoint for the tool
23
+ * @param authRequirements Authentication requirements (optional)
24
+ */
25
+ registerTool(name: string, description: string, handler: any, // Changed from Function to any to avoid confusion with built-in Function type
26
+ parameters: Parameter[], endpoint: string, authRequirements?: AuthRequirement[]): void;
27
+ }
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ToolsService = void 0;
7
+ const express_1 = __importDefault(require("express"));
8
+ const models_1 = require("./models");
9
+ const registry_1 = require("./registry");
10
+ class ToolsService {
11
+ /**
12
+ * Initialize a new tools service
13
+ * @param app Express application
14
+ */
15
+ constructor(app) {
16
+ this.functions = [];
17
+ this.app = app;
18
+ this.router = express_1.default.Router();
19
+ this.initRoutes();
20
+ // Register this service in the global registry
21
+ registry_1.registry.services.push(this);
22
+ }
23
+ /**
24
+ * Initialize the discovery endpoint
25
+ */
26
+ initRoutes() {
27
+ this.router.get('/discovery', (req, res) => {
28
+ res.json({ functions: this.functions.map(f => f.toJSON()) });
29
+ });
30
+ this.app.use(this.router);
31
+ }
32
+ /**
33
+ * Register a tool function
34
+ * @param name Tool name
35
+ * @param description Tool description
36
+ * @param handler Function implementing the tool
37
+ * @param parameters List of parameters for the tool
38
+ * @param endpoint API endpoint for the tool
39
+ * @param authRequirements Authentication requirements (optional)
40
+ */
41
+ registerTool(name, description, handler, // Changed from Function to any to avoid confusion with built-in Function type
42
+ parameters, endpoint, authRequirements) {
43
+ const func = new models_1.Function(name, description, parameters, endpoint, authRequirements);
44
+ this.functions.push(func);
45
+ // Register the actual endpoint
46
+ this.router.post(endpoint, async (req, res) => {
47
+ try {
48
+ console.log(`Received request for ${endpoint}:`, req.body);
49
+ // Extract parameters from the request body
50
+ let params;
51
+ if (req.body && req.body.parameters) {
52
+ // Standard format: { "parameters": { ... } }
53
+ params = req.body.parameters;
54
+ console.log(`Extracted parameters from 'parameters' key:`, params);
55
+ }
56
+ else {
57
+ // Fallback for direct testing: { "name": "value" }
58
+ console.log(`Warning: 'parameters' key not found in request body. Using body directly.`);
59
+ params = req.body;
60
+ }
61
+ // Extract auth data if available
62
+ const authData = req.body && req.body.auth;
63
+ if (authData) {
64
+ console.log(`Auth data provided for provider: ${authData.provider || 'unknown'}`);
65
+ }
66
+ // Call the handler with extracted parameters and auth data
67
+ // Check if handler accepts auth as third parameter
68
+ const handlerParamCount = handler.length;
69
+ let result;
70
+ if (handlerParamCount >= 2) {
71
+ // Handler accepts auth data
72
+ result = await handler(params, authData);
73
+ }
74
+ else {
75
+ // Handler doesn't accept auth data
76
+ result = await handler(params);
77
+ }
78
+ console.log(`Tool ${name} returned:`, result);
79
+ res.json(result);
80
+ }
81
+ catch (error) {
82
+ console.error(`Error in tool ${name}:`, error);
83
+ res.status(500).json({ error: error.message || 'Unknown error' });
84
+ }
85
+ });
86
+ }
87
+ }
88
+ exports.ToolsService = ToolsService;
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@optimizely-opal/opal-tools-sdk",
3
+ "version": "0.1.0-dev",
4
+ "description": "SDK for creating Opal-compatible tools services",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "test": "jest",
10
+ "prepublishOnly": "npm run build"
11
+ },
12
+ "keywords": [
13
+ "opal",
14
+ "tools",
15
+ "sdk",
16
+ "ai",
17
+ "llm"
18
+ ],
19
+ "author": "Optimizely",
20
+ "license": "MIT",
21
+ "dependencies": {
22
+ "express": "^4.18.2",
23
+ "axios": "^1.6.0",
24
+ "reflect-metadata": "^0.1.13"
25
+ },
26
+ "devDependencies": {
27
+ "@types/express": "^4.17.17",
28
+ "@types/jest": "^29.5.3",
29
+ "@types/node": "^20.4.5",
30
+ "jest": "^29.6.2",
31
+ "ts-jest": "^29.1.1",
32
+ "typescript": "^5.1.6"
33
+ },
34
+ "peerDependencies": {
35
+ "express": "^4.18.2"
36
+ },
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/optimizely/opal-tools-sdk.git"
40
+ },
41
+ "bugs": {
42
+ "url": "https://github.com/optimizely/opal-tools-sdk/issues"
43
+ },
44
+ "homepage": "https://github.com/optimizely/opal-tools-sdk#readme"
45
+ }
package/src/auth.ts ADDED
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Middleware to handle authentication requirements
3
+ * @param req Express request
4
+ * @param res Express response
5
+ * @param next Express next function
6
+ */
7
+ export function authMiddleware(req: any, res: any, next: any) {
8
+ const authHeader = req.headers.authorization;
9
+ if (!authHeader && req.authRequired) {
10
+ return res.status(401).json({ error: 'Authentication required' });
11
+ }
12
+
13
+ // The Tools Management Service will provide the appropriate token
14
+ // in the Authorization header
15
+ next();
16
+ }
17
+
18
+ interface AuthOptions {
19
+ provider: string;
20
+ scopeBundle: string;
21
+ required?: boolean;
22
+ }
23
+
24
+ /**
25
+ * Decorator to indicate that a tool requires authentication
26
+ * @param options Authentication options
27
+ */
28
+ export function requiresAuth(options: AuthOptions) {
29
+ return function(target: any, propertyKey?: string, descriptor?: PropertyDescriptor) {
30
+ const isMethod = propertyKey && descriptor;
31
+
32
+ if (isMethod && descriptor) {
33
+ const originalMethod = descriptor.value;
34
+
35
+ descriptor.value = function(...args: any[]) {
36
+ // Store auth requirements in function metadata
37
+ const fn = originalMethod as any;
38
+ fn.__authRequirements__ = {
39
+ provider: options.provider,
40
+ scopeBundle: options.scopeBundle,
41
+ required: options.required ?? true
42
+ };
43
+
44
+ return originalMethod.apply(this, args);
45
+ };
46
+
47
+ return descriptor;
48
+ }
49
+
50
+ return target;
51
+ };
52
+ }
@@ -0,0 +1,134 @@
1
+ import 'reflect-metadata';
2
+ import { ParameterType, Parameter, AuthRequirement } from './models';
3
+ import { registry } from './registry';
4
+
5
+ interface ParameterDefinition {
6
+ name: string;
7
+ type: ParameterType;
8
+ description: string;
9
+ required: boolean;
10
+ }
11
+
12
+ interface ToolOptions {
13
+ name: string;
14
+ description: string;
15
+ parameters?: ParameterDefinition[];
16
+ authRequirements?: {
17
+ provider: string;
18
+ scopeBundle: string;
19
+ required?: boolean;
20
+ };
21
+ }
22
+
23
+ /**
24
+ * Map a TypeScript type to a ParameterType
25
+ * @param type TypeScript type
26
+ */
27
+ function mapTypeToParameterType(type: any): ParameterType {
28
+ if (type === String || type.name === 'String') {
29
+ return ParameterType.String;
30
+ } else if (type === Number || type.name === 'Number') {
31
+ return ParameterType.Number;
32
+ } else if (type === Boolean || type.name === 'Boolean') {
33
+ return ParameterType.Boolean;
34
+ } else if (type === Array || type.name === 'Array') {
35
+ return ParameterType.List;
36
+ } else if (type === Object || type.name === 'Object') {
37
+ return ParameterType.Dictionary;
38
+ }
39
+
40
+ // Default to string
41
+ return ParameterType.String;
42
+ }
43
+
44
+ /**
45
+ * Extract parameters from a TypeScript interface
46
+ * @param paramType Parameter type object
47
+ */
48
+ function extractParameters(paramType: any): Parameter[] {
49
+ const parameters: Parameter[] = [];
50
+
51
+ // This is very basic and doesn't handle complex types
52
+ // For production use, this would need to be more sophisticated
53
+ for (const key in paramType) {
54
+ if (paramType.hasOwnProperty(key)) {
55
+ const type = typeof paramType[key] === 'undefined' ? String : paramType[key].constructor;
56
+ const required = true; // In a real implementation, we'd detect optional parameters
57
+
58
+ parameters.push(new Parameter(
59
+ key,
60
+ mapTypeToParameterType(type),
61
+ '', // Description - in a real impl we'd use TypeDoc or similar
62
+ required
63
+ ));
64
+ }
65
+ }
66
+
67
+ return parameters;
68
+ }
69
+
70
+ /**
71
+ * Decorator to register a function as an Opal tool
72
+ * @param options Tool options including:
73
+ * - name: Name of the tool
74
+ * - description: Description of the tool
75
+ * - authRequirements: (Optional) Authentication requirements
76
+ * Format: { provider: "oauth_provider", scopeBundle: "permissions_scope", required: true }
77
+ * Example: { provider: "google", scopeBundle: "calendar", required: true }
78
+ *
79
+ * Note: If your tool requires authentication, define your handler function with two parameters:
80
+ * ```
81
+ * async function myTool(parameters: ParameterInterface, authData?: any): Promise<any> {
82
+ * // Your tool implementation
83
+ * }
84
+ * ```
85
+ */
86
+ export function tool(options: ToolOptions) {
87
+ return function(target: any, propertyKey?: string, descriptor?: PropertyDescriptor) {
88
+ const isMethod = propertyKey && descriptor;
89
+ const handler = isMethod ? descriptor.value : target;
90
+
91
+ // Generate endpoint from name - ensure hyphens instead of underscores
92
+ const endpoint = `/tools/${options.name.replace(/_/g, '-')}`;
93
+
94
+ // Convert parameter definitions to Parameter objects
95
+ const parameters: Parameter[] = [];
96
+ if (options.parameters && options.parameters.length > 0) {
97
+ // Use the explicitly provided parameter definitions
98
+ for (const paramDef of options.parameters) {
99
+ parameters.push(new Parameter(
100
+ paramDef.name,
101
+ paramDef.type,
102
+ paramDef.description,
103
+ paramDef.required
104
+ ));
105
+ }
106
+ }
107
+
108
+ // Create auth requirements if specified
109
+ let authRequirements: AuthRequirement[] | undefined;
110
+ if (options.authRequirements) {
111
+ authRequirements = [
112
+ new AuthRequirement(
113
+ options.authRequirements.provider,
114
+ options.authRequirements.scopeBundle,
115
+ options.authRequirements.required ?? true
116
+ )
117
+ ];
118
+ }
119
+
120
+ // Register the tool with all services
121
+ for (const service of registry.services) {
122
+ service.registerTool(
123
+ options.name,
124
+ options.description,
125
+ handler,
126
+ parameters,
127
+ endpoint,
128
+ authRequirements
129
+ );
130
+ }
131
+
132
+ return isMethod ? descriptor : target;
133
+ };
134
+ }
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ import 'reflect-metadata';
2
+
3
+ export { ToolsService } from './service';
4
+ export { tool } from './decorators';
5
+ export { requiresAuth } from './auth';
6
+ export * from './models';
package/src/models.ts ADDED
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Types of parameters supported by Opal tools
3
+ */
4
+ export enum ParameterType {
5
+ String = 'string',
6
+ Integer = 'integer',
7
+ Number = 'number',
8
+ Boolean = 'boolean',
9
+ List = 'list',
10
+ Dictionary = 'object'
11
+ }
12
+
13
+ /**
14
+ * Parameter definition for an Opal tool
15
+ */
16
+ export class Parameter {
17
+ /**
18
+ * Create a new parameter definition
19
+ * @param name Parameter name
20
+ * @param type Parameter type
21
+ * @param description Parameter description
22
+ * @param required Whether the parameter is required
23
+ */
24
+ constructor(
25
+ public name: string,
26
+ public type: ParameterType,
27
+ public description: string,
28
+ public required: boolean
29
+ ) {}
30
+
31
+ /**
32
+ * Convert to JSON for the discovery endpoint
33
+ */
34
+ toJSON() {
35
+ return {
36
+ name: this.name,
37
+ type: this.type,
38
+ description: this.description,
39
+ required: this.required
40
+ };
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Authentication requirements for an Opal tool
46
+ */
47
+ export class AuthRequirement {
48
+ /**
49
+ * Create a new authentication requirement
50
+ * @param provider Auth provider (e.g., "google", "microsoft")
51
+ * @param scopeBundle Scope bundle (e.g., "calendar", "drive")
52
+ * @param required Whether authentication is required
53
+ */
54
+ constructor(
55
+ public provider: string,
56
+ public scopeBundle: string,
57
+ public required: boolean = true
58
+ ) {}
59
+
60
+ /**
61
+ * Convert to JSON for the discovery endpoint
62
+ */
63
+ toJSON() {
64
+ return {
65
+ provider: this.provider,
66
+ scope_bundle: this.scopeBundle,
67
+ required: this.required
68
+ };
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Function definition for an Opal tool
74
+ */
75
+ export class Function {
76
+ /**
77
+ * HTTP method for the endpoint (default: POST)
78
+ */
79
+ public httpMethod: string = 'POST';
80
+
81
+ /**
82
+ * Create a new function definition
83
+ * @param name Function name
84
+ * @param description Function description
85
+ * @param parameters Function parameters
86
+ * @param endpoint API endpoint
87
+ * @param authRequirements Authentication requirements (optional)
88
+ */
89
+ constructor(
90
+ public name: string,
91
+ public description: string,
92
+ public parameters: Parameter[],
93
+ public endpoint: string,
94
+ public authRequirements?: AuthRequirement[]
95
+ ) {}
96
+
97
+ /**
98
+ * Convert to JSON for the discovery endpoint
99
+ */
100
+ toJSON() {
101
+ const result: any = {
102
+ name: this.name,
103
+ description: this.description,
104
+ parameters: this.parameters.map(p => p.toJSON()),
105
+ endpoint: this.endpoint,
106
+ http_method: this.httpMethod
107
+ };
108
+
109
+ if (this.authRequirements && this.authRequirements.length > 0) {
110
+ result.auth_requirements = this.authRequirements.map(auth => auth.toJSON());
111
+ }
112
+
113
+ return result;
114
+ }
115
+ }
@@ -0,0 +1,8 @@
1
+ import { ToolsService } from './service';
2
+
3
+ /**
4
+ * Internal registry for ToolsService instances
5
+ */
6
+ export const registry = {
7
+ services: [] as ToolsService[]
8
+ };
package/src/service.ts ADDED
@@ -0,0 +1,98 @@
1
+ import express, { Express, Request, Response, Router } from 'express';
2
+ import { Function, AuthRequirement, Parameter } from './models';
3
+ import { registry } from './registry';
4
+
5
+ export class ToolsService {
6
+ private app: Express;
7
+ private router: Router;
8
+ private functions: Function[] = [];
9
+
10
+ /**
11
+ * Initialize a new tools service
12
+ * @param app Express application
13
+ */
14
+ constructor(app: Express) {
15
+ this.app = app;
16
+ this.router = express.Router();
17
+ this.initRoutes();
18
+
19
+ // Register this service in the global registry
20
+ registry.services.push(this);
21
+ }
22
+
23
+ /**
24
+ * Initialize the discovery endpoint
25
+ */
26
+ private initRoutes(): void {
27
+ this.router.get('/discovery', (req: Request, res: Response) => {
28
+ res.json({ functions: this.functions.map(f => f.toJSON()) });
29
+ });
30
+
31
+ this.app.use(this.router);
32
+ }
33
+
34
+ /**
35
+ * Register a tool function
36
+ * @param name Tool name
37
+ * @param description Tool description
38
+ * @param handler Function implementing the tool
39
+ * @param parameters List of parameters for the tool
40
+ * @param endpoint API endpoint for the tool
41
+ * @param authRequirements Authentication requirements (optional)
42
+ */
43
+ registerTool(
44
+ name: string,
45
+ description: string,
46
+ handler: any, // Changed from Function to any to avoid confusion with built-in Function type
47
+ parameters: Parameter[],
48
+ endpoint: string,
49
+ authRequirements?: AuthRequirement[]
50
+ ): void {
51
+ const func = new Function(name, description, parameters, endpoint, authRequirements);
52
+ this.functions.push(func);
53
+
54
+ // Register the actual endpoint
55
+ this.router.post(endpoint, async (req: Request, res: Response) => {
56
+ try {
57
+ console.log(`Received request for ${endpoint}:`, req.body);
58
+
59
+ // Extract parameters from the request body
60
+ let params;
61
+ if (req.body && req.body.parameters) {
62
+ // Standard format: { "parameters": { ... } }
63
+ params = req.body.parameters;
64
+ console.log(`Extracted parameters from 'parameters' key:`, params);
65
+ } else {
66
+ // Fallback for direct testing: { "name": "value" }
67
+ console.log(`Warning: 'parameters' key not found in request body. Using body directly.`);
68
+ params = req.body;
69
+ }
70
+
71
+ // Extract auth data if available
72
+ const authData = req.body && req.body.auth;
73
+ if (authData) {
74
+ console.log(`Auth data provided for provider: ${authData.provider || 'unknown'}`);
75
+ }
76
+
77
+ // Call the handler with extracted parameters and auth data
78
+ // Check if handler accepts auth as third parameter
79
+ const handlerParamCount = handler.length;
80
+ let result;
81
+
82
+ if (handlerParamCount >= 2) {
83
+ // Handler accepts auth data
84
+ result = await handler(params, authData);
85
+ } else {
86
+ // Handler doesn't accept auth data
87
+ result = await handler(params);
88
+ }
89
+
90
+ console.log(`Tool ${name} returned:`, result);
91
+ res.json(result);
92
+ } catch (error: any) {
93
+ console.error(`Error in tool ${name}:`, error);
94
+ res.status(500).json({ error: error.message || 'Unknown error' });
95
+ }
96
+ });
97
+ }
98
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es2020",
4
+ "module": "commonjs",
5
+ "declaration": true,
6
+ "outDir": "./dist",
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "forceConsistentCasingInFileNames": true,
11
+ "experimentalDecorators": true,
12
+ "emitDecoratorMetadata": true
13
+ },
14
+ "include": ["src"],
15
+ "exclude": ["node_modules", "**/*.test.ts"]
16
+ }