@optimizely-opal/opal-tools-sdk 0.1.6-dev → 0.1.9-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 +201 -216
- package/dist/decorators.d.ts +2 -0
- package/dist/decorators.js +2 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +5 -4
- package/dist/models.d.ts +22 -1
- package/dist/models.js +28 -2
- package/dist/proteus.d.ts +1587 -0
- package/dist/proteus.js +98 -0
- package/dist/registerResource.d.ts +60 -0
- package/dist/registerResource.js +59 -0
- package/dist/registerTool.d.ts +18 -7
- package/dist/registerTool.js +52 -3
- package/dist/service.d.ts +15 -3
- package/dist/service.js +80 -21
- package/package.json +2 -2
- package/scripts/generate-proteus.ts +135 -0
- package/src/decorators.ts +4 -0
- package/src/index.ts +3 -2
- package/src/models.ts +27 -0
- package/src/proteus.ts +2314 -0
- package/src/registerResource.ts +82 -0
- package/src/registerTool.ts +19 -29
- package/src/service.ts +110 -23
- package/tests/integration.test.ts +252 -73
- package/tests/proteus.test.ts +122 -0
- package/dist/block.d.ts +0 -4760
- package/dist/block.js +0 -104
- package/scripts/generate-block.ts +0 -167
- package/src/block.ts +0 -11761
- package/tests/block.test.ts +0 -115
package/dist/proteus.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Generated by json-schema-to-typescript
|
|
4
|
+
* DO NOT MODIFY - This file is auto-generated from proteus-document-spec.json
|
|
5
|
+
* Run 'npm run generate:proteus' to regenerate
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.UI = void 0;
|
|
9
|
+
/**
|
|
10
|
+
* Builder namespace for Adaptive UI Document components.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* UI.Document({ children: [...] })
|
|
14
|
+
* UI.Heading({ children: "Title", level: "2" })
|
|
15
|
+
* UI.Input({ name: "field_name", placeholder: "Enter..." })
|
|
16
|
+
*/
|
|
17
|
+
exports.UI = {
|
|
18
|
+
Action: (props) => ({
|
|
19
|
+
$type: "Action",
|
|
20
|
+
...props,
|
|
21
|
+
}),
|
|
22
|
+
Badge: (props) => ({
|
|
23
|
+
$type: "Badge",
|
|
24
|
+
...props,
|
|
25
|
+
}),
|
|
26
|
+
CancelAction: (props) => ({ $type: "CancelAction", ...props }),
|
|
27
|
+
Chart: (props) => ({
|
|
28
|
+
$type: "Chart",
|
|
29
|
+
...props,
|
|
30
|
+
}),
|
|
31
|
+
DataTable: (props) => ({
|
|
32
|
+
$type: "DataTable",
|
|
33
|
+
...props,
|
|
34
|
+
}),
|
|
35
|
+
Document: (props) => ({
|
|
36
|
+
$type: "Document",
|
|
37
|
+
...props,
|
|
38
|
+
}),
|
|
39
|
+
Field: (props) => ({
|
|
40
|
+
$type: "Field",
|
|
41
|
+
...props,
|
|
42
|
+
}),
|
|
43
|
+
Group: (props) => ({
|
|
44
|
+
$type: "Group",
|
|
45
|
+
...props,
|
|
46
|
+
}),
|
|
47
|
+
Heading: (props) => ({
|
|
48
|
+
$type: "Heading",
|
|
49
|
+
...props,
|
|
50
|
+
}),
|
|
51
|
+
Image: (props) => ({
|
|
52
|
+
$type: "Image",
|
|
53
|
+
...props,
|
|
54
|
+
}),
|
|
55
|
+
Input: (props) => ({
|
|
56
|
+
$type: "Input",
|
|
57
|
+
...props,
|
|
58
|
+
}),
|
|
59
|
+
Link: (props) => ({
|
|
60
|
+
$type: "Link",
|
|
61
|
+
...props,
|
|
62
|
+
}),
|
|
63
|
+
Map: (props) => ({
|
|
64
|
+
$type: "Map",
|
|
65
|
+
...props,
|
|
66
|
+
}),
|
|
67
|
+
MIME_TYPE: "application/vnd.opal.proteus+json",
|
|
68
|
+
Range: (props) => ({
|
|
69
|
+
$type: "Range",
|
|
70
|
+
...props,
|
|
71
|
+
}),
|
|
72
|
+
Select: (props) => ({
|
|
73
|
+
$type: "Select",
|
|
74
|
+
...props,
|
|
75
|
+
}),
|
|
76
|
+
SelectContent: (props) => ({ $type: "SelectContent", ...props }),
|
|
77
|
+
SelectTrigger: (props) => ({ $type: "SelectTrigger", ...props }),
|
|
78
|
+
Separator: (props) => ({
|
|
79
|
+
$type: "Separator",
|
|
80
|
+
...props,
|
|
81
|
+
}),
|
|
82
|
+
Show: (props) => ({
|
|
83
|
+
$type: "Show",
|
|
84
|
+
...props,
|
|
85
|
+
}),
|
|
86
|
+
Text: (props) => ({
|
|
87
|
+
$type: "Text",
|
|
88
|
+
...props,
|
|
89
|
+
}),
|
|
90
|
+
Textarea: (props) => ({
|
|
91
|
+
$type: "Textarea",
|
|
92
|
+
...props,
|
|
93
|
+
}),
|
|
94
|
+
Value: (props) => ({
|
|
95
|
+
$type: "Value",
|
|
96
|
+
...props,
|
|
97
|
+
}),
|
|
98
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { ProteusDocument } from "./proteus";
|
|
2
|
+
type ResourceOptions = {
|
|
3
|
+
/** Description of the resource */
|
|
4
|
+
description?: string;
|
|
5
|
+
/** MIME type of the resource content (e.g., "application/vnd.opal.proteus+json") */
|
|
6
|
+
mimeType?: string;
|
|
7
|
+
/** Human-readable title for the resource */
|
|
8
|
+
title?: string;
|
|
9
|
+
/** The unique URI for this resource (e.g., "ui://my-app/create-form") */
|
|
10
|
+
uri: string;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Register a function as an MCP resource
|
|
14
|
+
*
|
|
15
|
+
* The handler can return either a string or a UI.Document. When returning a UI.Document,
|
|
16
|
+
* the SDK will automatically serialize it to JSON and set the MIME type to
|
|
17
|
+
* "application/vnd.opal.proteus+json" (if not already specified).
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* // Example 1: Returning a string (manual serialization)
|
|
22
|
+
* const getFormResource = registerResource('create-form', {
|
|
23
|
+
* uri: 'ui://my-app/create-form',
|
|
24
|
+
* description: 'Create form UI specification',
|
|
25
|
+
* mimeType: 'application/vnd.opal.proteus+json',
|
|
26
|
+
* title: 'Create Form'
|
|
27
|
+
* }, async () => {
|
|
28
|
+
* return JSON.stringify(proteusSpec);
|
|
29
|
+
* });
|
|
30
|
+
*
|
|
31
|
+
* // Example 2: Returning a UI.Document (automatic serialization)
|
|
32
|
+
* import { UI } from '@optimizely-opal/opal-tools-sdk';
|
|
33
|
+
*
|
|
34
|
+
* const getDynamicForm = registerResource('create-form', {
|
|
35
|
+
* uri: 'ui://my-app/create-form',
|
|
36
|
+
* description: 'Create form UI specification',
|
|
37
|
+
* title: 'Create Form'
|
|
38
|
+
* }, async () => {
|
|
39
|
+
* return UI.Document({
|
|
40
|
+
* appName: 'Item Manager',
|
|
41
|
+
* title: 'Create New Item',
|
|
42
|
+
* body: [
|
|
43
|
+
* UI.Heading({ children: 'Create Item' }),
|
|
44
|
+
* UI.Field({
|
|
45
|
+
* label: 'Name',
|
|
46
|
+
* children: UI.Input({ name: 'item_name' })
|
|
47
|
+
* })
|
|
48
|
+
* ],
|
|
49
|
+
* actions: [UI.Action({ children: 'Save' })]
|
|
50
|
+
* });
|
|
51
|
+
* });
|
|
52
|
+
* ```
|
|
53
|
+
*
|
|
54
|
+
* @param name - Name of the resource
|
|
55
|
+
* @param options - Resource options (uri, description, mimeType, title)
|
|
56
|
+
* @param handler - Async function that returns the resource content (string or UI.Document)
|
|
57
|
+
* @returns The handler function (for convenience)
|
|
58
|
+
*/
|
|
59
|
+
export declare function registerResource(name: string, options: ResourceOptions, handler: () => Promise<ProteusDocument | string> | ProteusDocument | string): typeof handler;
|
|
60
|
+
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerResource = registerResource;
|
|
4
|
+
const registry_1 = require("./registry");
|
|
5
|
+
/**
|
|
6
|
+
* Register a function as an MCP resource
|
|
7
|
+
*
|
|
8
|
+
* The handler can return either a string or a UI.Document. When returning a UI.Document,
|
|
9
|
+
* the SDK will automatically serialize it to JSON and set the MIME type to
|
|
10
|
+
* "application/vnd.opal.proteus+json" (if not already specified).
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* // Example 1: Returning a string (manual serialization)
|
|
15
|
+
* const getFormResource = registerResource('create-form', {
|
|
16
|
+
* uri: 'ui://my-app/create-form',
|
|
17
|
+
* description: 'Create form UI specification',
|
|
18
|
+
* mimeType: 'application/vnd.opal.proteus+json',
|
|
19
|
+
* title: 'Create Form'
|
|
20
|
+
* }, async () => {
|
|
21
|
+
* return JSON.stringify(proteusSpec);
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* // Example 2: Returning a UI.Document (automatic serialization)
|
|
25
|
+
* import { UI } from '@optimizely-opal/opal-tools-sdk';
|
|
26
|
+
*
|
|
27
|
+
* const getDynamicForm = registerResource('create-form', {
|
|
28
|
+
* uri: 'ui://my-app/create-form',
|
|
29
|
+
* description: 'Create form UI specification',
|
|
30
|
+
* title: 'Create Form'
|
|
31
|
+
* }, async () => {
|
|
32
|
+
* return UI.Document({
|
|
33
|
+
* appName: 'Item Manager',
|
|
34
|
+
* title: 'Create New Item',
|
|
35
|
+
* body: [
|
|
36
|
+
* UI.Heading({ children: 'Create Item' }),
|
|
37
|
+
* UI.Field({
|
|
38
|
+
* label: 'Name',
|
|
39
|
+
* children: UI.Input({ name: 'item_name' })
|
|
40
|
+
* })
|
|
41
|
+
* ],
|
|
42
|
+
* actions: [UI.Action({ children: 'Save' })]
|
|
43
|
+
* });
|
|
44
|
+
* });
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* @param name - Name of the resource
|
|
48
|
+
* @param options - Resource options (uri, description, mimeType, title)
|
|
49
|
+
* @param handler - Async function that returns the resource content (string or UI.Document)
|
|
50
|
+
* @returns The handler function (for convenience)
|
|
51
|
+
*/
|
|
52
|
+
function registerResource(name, options, handler) {
|
|
53
|
+
console.log(`Registering resource ${name} with URI ${options.uri}`);
|
|
54
|
+
// Register the resource with all services
|
|
55
|
+
for (const service of registry_1.registry.services) {
|
|
56
|
+
service.registerResource(options.uri, name, options.description, options.mimeType, options.title, handler);
|
|
57
|
+
}
|
|
58
|
+
return handler;
|
|
59
|
+
}
|
package/dist/registerTool.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { z } from "zod/v4";
|
|
2
|
-
import type { BlockResponse } from "./block";
|
|
3
2
|
import { Credentials } from "./models";
|
|
4
3
|
/**
|
|
5
4
|
* Extra context passed to tool handlers
|
|
@@ -13,7 +12,7 @@ export type RequestHandlerExtra = {
|
|
|
13
12
|
/** Execution mode: 'headless' for non-interactive, 'interactive' for user interaction (defaults to 'headless') */
|
|
14
13
|
mode: "headless" | "interactive";
|
|
15
14
|
};
|
|
16
|
-
type ToolOptions<TSchema extends Record<string, z.ZodTypeAny
|
|
15
|
+
type ToolOptions<TSchema extends Record<string, z.ZodTypeAny>> = {
|
|
17
16
|
authRequirements?: {
|
|
18
17
|
provider: string;
|
|
19
18
|
required?: boolean;
|
|
@@ -21,7 +20,7 @@ type ToolOptions<TSchema extends Record<string, z.ZodTypeAny>, TType extends "bl
|
|
|
21
20
|
};
|
|
22
21
|
description: string;
|
|
23
22
|
inputSchema: TSchema;
|
|
24
|
-
|
|
23
|
+
uiResource?: string;
|
|
25
24
|
};
|
|
26
25
|
/**
|
|
27
26
|
* Register a function as an Opal tool
|
|
@@ -58,11 +57,23 @@ type ToolOptions<TSchema extends Record<string, z.ZodTypeAny>, TType extends "bl
|
|
|
58
57
|
* return fetch(params.url, { headers });
|
|
59
58
|
* });
|
|
60
59
|
* ```
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```typescript
|
|
63
|
+
* // With UI resource for dynamic rendering
|
|
64
|
+
* const createTaskTool = registerTool('create_task', {
|
|
65
|
+
* description: 'Create a new task',
|
|
66
|
+
* inputSchema: {
|
|
67
|
+
* title: z.string().describe('Task title'),
|
|
68
|
+
* description: z.string().describe('Task description')
|
|
69
|
+
* },
|
|
70
|
+
* uiResource: 'ui://my-app/create-form'
|
|
71
|
+
* }, async (params) => {
|
|
72
|
+
* return { id: 'task-123', ...params };
|
|
73
|
+
* });
|
|
74
|
+
* ```
|
|
61
75
|
*/
|
|
62
|
-
export declare function registerTool<TSchema extends Record<string, z.ZodTypeAny>>(name: string, options: ToolOptions<TSchema
|
|
63
|
-
[K in keyof TSchema]: z.infer<TSchema[K]>;
|
|
64
|
-
}, extra?: RequestHandlerExtra) => BlockResponse | Promise<BlockResponse>): typeof handler;
|
|
65
|
-
export declare function registerTool<TSchema extends Record<string, z.ZodTypeAny>>(name: string, options: Omit<ToolOptions<TSchema>, "type"> | ToolOptions<TSchema, "json">, handler: (params: {
|
|
76
|
+
export declare function registerTool<TSchema extends Record<string, z.ZodTypeAny>>(name: string, options: ToolOptions<TSchema>, handler: (params: {
|
|
66
77
|
[K in keyof TSchema]: z.infer<TSchema[K]>;
|
|
67
78
|
}, extra?: RequestHandlerExtra) => Promise<unknown> | unknown): typeof handler;
|
|
68
79
|
export {};
|
package/dist/registerTool.js
CHANGED
|
@@ -4,16 +4,65 @@ exports.registerTool = registerTool;
|
|
|
4
4
|
const v4_1 = require("zod/v4");
|
|
5
5
|
const models_1 = require("./models");
|
|
6
6
|
const registry_1 = require("./registry");
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Register a function as an Opal tool
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* const greetTool = registerTool('greet', {
|
|
13
|
+
* description: 'Greet a user',
|
|
14
|
+
* inputSchema: {
|
|
15
|
+
* name: z.string().describe('The name to greet')
|
|
16
|
+
* }
|
|
17
|
+
* }, async (params) => {
|
|
18
|
+
* // params is automatically typed as { name: string }
|
|
19
|
+
* return `Hello, ${params.name}!`;
|
|
20
|
+
* });
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```typescript
|
|
25
|
+
* // With auth and execution mode
|
|
26
|
+
* const fetchTool = registerTool('fetch_data', {
|
|
27
|
+
* description: 'Fetch data from API',
|
|
28
|
+
* inputSchema: {
|
|
29
|
+
* url: z.string().describe('URL to fetch')
|
|
30
|
+
* },
|
|
31
|
+
* authRequirements: {
|
|
32
|
+
* provider: 'api-service',
|
|
33
|
+
* scopeBundle: 'read'
|
|
34
|
+
* }
|
|
35
|
+
* }, async (params, extra) => {
|
|
36
|
+
* // extra.mode: 'headless' | 'interactive'
|
|
37
|
+
* // extra.auth: { provider, credentials }
|
|
38
|
+
* const headers = extra?.auth ? { Authorization: extra.auth.credentials.token } : {};
|
|
39
|
+
* return fetch(params.url, { headers });
|
|
40
|
+
* });
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```typescript
|
|
45
|
+
* // With UI resource for dynamic rendering
|
|
46
|
+
* const createTaskTool = registerTool('create_task', {
|
|
47
|
+
* description: 'Create a new task',
|
|
48
|
+
* inputSchema: {
|
|
49
|
+
* title: z.string().describe('Task title'),
|
|
50
|
+
* description: z.string().describe('Task description')
|
|
51
|
+
* },
|
|
52
|
+
* uiResource: 'ui://my-app/create-form'
|
|
53
|
+
* }, async (params) => {
|
|
54
|
+
* return { id: 'task-123', ...params };
|
|
55
|
+
* });
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
8
58
|
function registerTool(name, options, handler) {
|
|
9
59
|
// Register the tool with all services
|
|
10
|
-
const responseType = options.type || "json";
|
|
11
60
|
for (const service of registry_1.registry.services) {
|
|
12
61
|
service.registerTool(name, options.description, handler, jsonSchemaToParameters(v4_1.z.toJSONSchema(v4_1.z.object(options.inputSchema))), `/tools/${name.replace(/_/g, "-")}`, options.authRequirements
|
|
13
62
|
? [
|
|
14
63
|
new models_1.AuthRequirement(options.authRequirements.provider, options.authRequirements.scopeBundle, options.authRequirements.required ?? true),
|
|
15
64
|
]
|
|
16
|
-
: undefined,
|
|
65
|
+
: undefined, true, options.uiResource);
|
|
17
66
|
}
|
|
18
67
|
return handler;
|
|
19
68
|
}
|
package/dist/service.d.ts
CHANGED
|
@@ -1,14 +1,26 @@
|
|
|
1
1
|
import { Express } from "express";
|
|
2
2
|
import { AuthRequirement, Parameter } from "./models";
|
|
3
|
+
import { ProteusDocument } from "./proteus";
|
|
3
4
|
export declare class ToolsService {
|
|
4
5
|
private app;
|
|
5
6
|
private functions;
|
|
7
|
+
private resources;
|
|
6
8
|
private router;
|
|
7
9
|
/**
|
|
8
10
|
* Initialize a new tools service
|
|
9
11
|
* @param app Express application
|
|
10
12
|
*/
|
|
11
13
|
constructor(app: Express);
|
|
14
|
+
/**
|
|
15
|
+
* Register a resource function
|
|
16
|
+
* @param uri The unique URI for this resource (e.g., "ui://my-app/create-form")
|
|
17
|
+
* @param name Name of the resource
|
|
18
|
+
* @param description Description of the resource (optional)
|
|
19
|
+
* @param mimeType MIME type of the resource content (optional)
|
|
20
|
+
* @param title Human-readable title for the resource (optional)
|
|
21
|
+
* @param handler Function implementing the resource (returns string or ProteusDocument)
|
|
22
|
+
*/
|
|
23
|
+
registerResource(uri: string, name: string, description: string | undefined, mimeType: string | undefined, title: string | undefined, handler: () => Promise<ProteusDocument | string> | ProteusDocument | string): void;
|
|
12
24
|
/**
|
|
13
25
|
* Register a tool function
|
|
14
26
|
* @param name Tool name
|
|
@@ -17,13 +29,13 @@ export declare class ToolsService {
|
|
|
17
29
|
* @param parameters List of parameters for the tool
|
|
18
30
|
* @param endpoint API endpoint for the tool
|
|
19
31
|
* @param authRequirements Authentication requirements (optional)
|
|
20
|
-
* @param responseType Response type - 'json' (default) or 'block'
|
|
21
32
|
* @param isNewStyle Whether this is a new-style tool (registerTool) vs legacy decorator
|
|
33
|
+
* @param uiResource URI of associated UI resource for dynamic rendering (optional)
|
|
22
34
|
*/
|
|
23
35
|
registerTool(name: string, description: string, handler: any, // Changed from Function to any to avoid confusion with built-in Function type
|
|
24
|
-
parameters: Parameter[], endpoint: string, authRequirements?: AuthRequirement[],
|
|
36
|
+
parameters: Parameter[], endpoint: string, authRequirements?: AuthRequirement[], isNewStyle?: boolean, uiResource?: string): void;
|
|
25
37
|
/**
|
|
26
|
-
* Initialize the discovery endpoint
|
|
38
|
+
* Initialize the discovery endpoint and resources/read endpoint
|
|
27
39
|
*/
|
|
28
40
|
private initRoutes;
|
|
29
41
|
}
|
package/dist/service.js
CHANGED
|
@@ -5,8 +5,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.ToolsService = void 0;
|
|
7
7
|
const express_1 = __importDefault(require("express"));
|
|
8
|
-
const block_1 = require("./block");
|
|
9
8
|
const models_1 = require("./models");
|
|
9
|
+
const proteus_1 = require("./proteus");
|
|
10
10
|
const registry_1 = require("./registry");
|
|
11
11
|
class ToolsService {
|
|
12
12
|
/**
|
|
@@ -15,12 +15,28 @@ class ToolsService {
|
|
|
15
15
|
*/
|
|
16
16
|
constructor(app) {
|
|
17
17
|
this.functions = [];
|
|
18
|
+
this.resources = new Map();
|
|
18
19
|
this.app = app;
|
|
19
20
|
this.router = express_1.default.Router();
|
|
20
21
|
this.initRoutes();
|
|
21
22
|
// Register this service in the global registry
|
|
22
23
|
registry_1.registry.services.push(this);
|
|
23
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Register a resource function
|
|
27
|
+
* @param uri The unique URI for this resource (e.g., "ui://my-app/create-form")
|
|
28
|
+
* @param name Name of the resource
|
|
29
|
+
* @param description Description of the resource (optional)
|
|
30
|
+
* @param mimeType MIME type of the resource content (optional)
|
|
31
|
+
* @param title Human-readable title for the resource (optional)
|
|
32
|
+
* @param handler Function implementing the resource (returns string or ProteusDocument)
|
|
33
|
+
*/
|
|
34
|
+
registerResource(uri, name, description, mimeType, title, handler) {
|
|
35
|
+
console.log(`Registering resource: ${name} with URI: ${uri}`);
|
|
36
|
+
const metadata = new models_1.Resource(uri, name, description, mimeType, title);
|
|
37
|
+
// Store both metadata and handler together
|
|
38
|
+
this.resources.set(uri, { handler, metadata });
|
|
39
|
+
}
|
|
24
40
|
/**
|
|
25
41
|
* Register a tool function
|
|
26
42
|
* @param name Tool name
|
|
@@ -29,17 +45,15 @@ class ToolsService {
|
|
|
29
45
|
* @param parameters List of parameters for the tool
|
|
30
46
|
* @param endpoint API endpoint for the tool
|
|
31
47
|
* @param authRequirements Authentication requirements (optional)
|
|
32
|
-
* @param responseType Response type - 'json' (default) or 'block'
|
|
33
48
|
* @param isNewStyle Whether this is a new-style tool (registerTool) vs legacy decorator
|
|
49
|
+
* @param uiResource URI of associated UI resource for dynamic rendering (optional)
|
|
34
50
|
*/
|
|
35
51
|
registerTool(name, description,
|
|
36
52
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
37
53
|
handler, // Changed from Function to any to avoid confusion with built-in Function type
|
|
38
|
-
parameters, endpoint, authRequirements,
|
|
39
|
-
const func = new models_1.Function(name, description, parameters, endpoint, authRequirements);
|
|
54
|
+
parameters, endpoint, authRequirements, isNewStyle = false, uiResource) {
|
|
55
|
+
const func = new models_1.Function(name, description, parameters, endpoint, authRequirements, uiResource);
|
|
40
56
|
this.functions.push(func);
|
|
41
|
-
// Determine if this is a block tool
|
|
42
|
-
const isBlockTool = responseType === "block";
|
|
43
57
|
// Register the actual endpoint
|
|
44
58
|
this.router.post(endpoint, async (req, res) => {
|
|
45
59
|
try {
|
|
@@ -82,18 +96,7 @@ class ToolsService {
|
|
|
82
96
|
}
|
|
83
97
|
}
|
|
84
98
|
console.log(`Tool ${name} returned:`, result);
|
|
85
|
-
|
|
86
|
-
if (isBlockTool) {
|
|
87
|
-
// Validate that block tools return a BlockResponse
|
|
88
|
-
if (!(0, block_1.isBlockResponse)(result)) {
|
|
89
|
-
throw new Error(`Block tool '${name}' must return a BlockResponse object, but returned ${typeof result}`);
|
|
90
|
-
}
|
|
91
|
-
res.set("Content-Type", "application/vnd.opal.block+json");
|
|
92
|
-
res.json(result);
|
|
93
|
-
}
|
|
94
|
-
else {
|
|
95
|
-
res.json(result);
|
|
96
|
-
}
|
|
99
|
+
res.json(result);
|
|
97
100
|
}
|
|
98
101
|
catch (error) {
|
|
99
102
|
console.error(`Error in tool ${name}:`, error);
|
|
@@ -104,11 +107,67 @@ class ToolsService {
|
|
|
104
107
|
});
|
|
105
108
|
}
|
|
106
109
|
/**
|
|
107
|
-
* Initialize the discovery endpoint
|
|
110
|
+
* Initialize the discovery endpoint and resources/read endpoint
|
|
108
111
|
*/
|
|
109
112
|
initRoutes() {
|
|
110
|
-
this.router.get("/discovery", (
|
|
111
|
-
res.json({
|
|
113
|
+
this.router.get("/discovery", (_req, res) => {
|
|
114
|
+
res.json({
|
|
115
|
+
functions: this.functions.map((f) => f.toJSON()),
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
// POST /resources/read endpoint for MCP protocol
|
|
119
|
+
this.router.post("/resources/read", async (req, res) => {
|
|
120
|
+
try {
|
|
121
|
+
const { uri } = req.body;
|
|
122
|
+
if (!uri) {
|
|
123
|
+
return res.status(400).json({
|
|
124
|
+
error: "Missing required field: uri",
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
console.log(`Received resource read request for URI: ${uri}`);
|
|
128
|
+
// Find the resource registration
|
|
129
|
+
const registration = this.resources.get(uri);
|
|
130
|
+
if (!registration) {
|
|
131
|
+
return res.status(404).json({
|
|
132
|
+
error: `Resource not found: ${uri}`,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
// Call handler and await result
|
|
136
|
+
const content = await registration.handler();
|
|
137
|
+
let textContent;
|
|
138
|
+
let mimeType = registration.metadata.mimeType;
|
|
139
|
+
// Check if handler returned a ProteusDocument
|
|
140
|
+
if (typeof content === "object" &&
|
|
141
|
+
content !== null &&
|
|
142
|
+
"$type" in content &&
|
|
143
|
+
content.$type === "Document") {
|
|
144
|
+
// Auto-serialize to JSON
|
|
145
|
+
textContent = JSON.stringify(content);
|
|
146
|
+
// Auto-set MIME type if not specified
|
|
147
|
+
if (!mimeType) {
|
|
148
|
+
mimeType = proteus_1.UI.MIME_TYPE;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
else if (typeof content === "string") {
|
|
152
|
+
textContent = content;
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
throw new Error(`Resource handler for '${uri}' must return a string or ProteusDocument, but returned ${typeof content}`);
|
|
156
|
+
}
|
|
157
|
+
console.log(`Resource ${uri} returned content of length: ${textContent.length}`);
|
|
158
|
+
// Return the resource content directly
|
|
159
|
+
res.json({
|
|
160
|
+
mimeType: mimeType || "text/plain",
|
|
161
|
+
text: textContent,
|
|
162
|
+
uri,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
console.error(`Error reading resource:`, error);
|
|
167
|
+
res.status(500).json({
|
|
168
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
169
|
+
});
|
|
170
|
+
}
|
|
112
171
|
});
|
|
113
172
|
this.app.use(this.router);
|
|
114
173
|
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@optimizely-opal/opal-tools-sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9-dev",
|
|
4
4
|
"description": "SDK for creating Opal-compatible tools services",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"build": "tsc -p tsconfig.build.json",
|
|
9
|
-
"generate:
|
|
9
|
+
"generate:proteus": "ts-node scripts/generate-proteus.ts",
|
|
10
10
|
"lint": "bash scripts/lint.sh",
|
|
11
11
|
"prepublishOnly": "npm run build",
|
|
12
12
|
"test": "vitest run"
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env ts-node
|
|
2
|
+
/**
|
|
3
|
+
* Generate Adaptive Proteus Document types from JSON schema.
|
|
4
|
+
*
|
|
5
|
+
* This script uses json-schema-to-typescript to generate TypeScript interfaces
|
|
6
|
+
* from the proteus-document-spec.json schema. The generated types will replace the
|
|
7
|
+
* manual Proteus builders once we're ready to switch over.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* npm run generate:proteus
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import * as fs from "fs";
|
|
14
|
+
import { compile } from "json-schema-to-typescript";
|
|
15
|
+
import * as path from "path";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Generate builder functions and ProteusResponse class.
|
|
19
|
+
*
|
|
20
|
+
* The generated TypeScript interfaces are great for validation, but we want to
|
|
21
|
+
* keep the builder API (UI.Document(), etc.) for ease of use.
|
|
22
|
+
*/
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
24
|
+
function generateBuilderCode(schema: any): string {
|
|
25
|
+
// Extract all Proteus components from definitions
|
|
26
|
+
const proteusComponents: Array<{ fullName: string; shortName: string }> = [];
|
|
27
|
+
const skipTypes = new Set([
|
|
28
|
+
"ProteusAtomicCondition",
|
|
29
|
+
"ProteusCondition",
|
|
30
|
+
"ProteusElement",
|
|
31
|
+
"ProteusEventHandler",
|
|
32
|
+
"ProteusNode",
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
if (schema.definitions) {
|
|
36
|
+
for (const componentName of Object.keys(schema.definitions)) {
|
|
37
|
+
if (
|
|
38
|
+
componentName.startsWith("Proteus") &&
|
|
39
|
+
!skipTypes.has(componentName)
|
|
40
|
+
) {
|
|
41
|
+
// Extract the short name (e.g., "Document" from "ProteusDocument")
|
|
42
|
+
const shortName = componentName.substring(7); // Remove "Proteus" prefix
|
|
43
|
+
proteusComponents.push({ fullName: componentName, shortName });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Sort components for consistent output
|
|
49
|
+
proteusComponents.sort((a, b) => a.shortName.localeCompare(b.shortName));
|
|
50
|
+
|
|
51
|
+
// Generate factory functions that return properly typed objects
|
|
52
|
+
const builderMethods = proteusComponents
|
|
53
|
+
.map(
|
|
54
|
+
({ fullName, shortName }) =>
|
|
55
|
+
` ${shortName}: (props: Omit<${fullName}, '$type'>): ${fullName} => ({ $type: '${shortName}' as const, ...props }),`,
|
|
56
|
+
)
|
|
57
|
+
.join("\n");
|
|
58
|
+
|
|
59
|
+
return `
|
|
60
|
+
/**
|
|
61
|
+
* Builder namespace for Adaptive UI Document components.
|
|
62
|
+
*
|
|
63
|
+
* Usage:
|
|
64
|
+
* UI.Document({ children: [...] })
|
|
65
|
+
* UI.Heading({ children: "Title", level: "2" })
|
|
66
|
+
* UI.Input({ name: "field_name", placeholder: "Enter..." })
|
|
67
|
+
*/
|
|
68
|
+
export const UI = {
|
|
69
|
+
MIME_TYPE: "application/vnd.opal.proteus+json" as const,
|
|
70
|
+
${builderMethods}
|
|
71
|
+
};
|
|
72
|
+
`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function main() {
|
|
76
|
+
// Paths
|
|
77
|
+
const scriptDir = __dirname;
|
|
78
|
+
const sdkRoot = path.join(scriptDir, "..");
|
|
79
|
+
const schemaFile = path.join(sdkRoot, "..", "proteus-document-spec.json");
|
|
80
|
+
const outputFile = path.join(sdkRoot, "src", "proteus.ts");
|
|
81
|
+
|
|
82
|
+
if (!fs.existsSync(schemaFile)) {
|
|
83
|
+
console.error(`Error: Schema file not found at ${schemaFile}`);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
// Read the schema
|
|
89
|
+
const schema = JSON.parse(fs.readFileSync(schemaFile, "utf-8"));
|
|
90
|
+
|
|
91
|
+
// Create a new schema that references all definitions to force their generation
|
|
92
|
+
const schemaWithExports = {
|
|
93
|
+
...schema,
|
|
94
|
+
definitions: schema.definitions,
|
|
95
|
+
// Export each definition by creating a oneOf at the root
|
|
96
|
+
oneOf: Object.keys(schema.definitions || {})
|
|
97
|
+
.filter((key) => key.startsWith("Proteus"))
|
|
98
|
+
.map((key) => ({ $ref: `#/definitions/${key}` })),
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// Generate TypeScript types
|
|
102
|
+
const ts = await compile(schemaWithExports, "ProteusDocumentSchema", {
|
|
103
|
+
bannerComment: `/**
|
|
104
|
+
* Generated by json-schema-to-typescript
|
|
105
|
+
* DO NOT MODIFY - This file is auto-generated from proteus-document-spec.json
|
|
106
|
+
* Run 'npm run generate:proteus' to regenerate
|
|
107
|
+
*/`,
|
|
108
|
+
declareExternallyReferenced: true,
|
|
109
|
+
strictIndexSignatures: true,
|
|
110
|
+
style: {
|
|
111
|
+
semi: true,
|
|
112
|
+
singleQuote: true,
|
|
113
|
+
},
|
|
114
|
+
unknownAny: true,
|
|
115
|
+
unreachableDefinitions: false,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Post-process to add builder functions
|
|
119
|
+
const finalContent = ts + "\n" + generateBuilderCode(schema);
|
|
120
|
+
|
|
121
|
+
// Write the output file
|
|
122
|
+
fs.writeFileSync(outputFile, finalContent, "utf-8");
|
|
123
|
+
|
|
124
|
+
console.log(`Generated: ${outputFile}`);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
console.error("Error: Generation failed");
|
|
127
|
+
console.error(error);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
main().catch((error) => {
|
|
133
|
+
console.error(error);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
});
|