@optimizely-opal/opal-tools-sdk 0.1.5-dev → 0.1.6-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/.prettierignore +5 -0
- package/.prettierrc +1 -0
- package/README.md +227 -202
- package/dist/auth.d.ts +5 -5
- package/dist/auth.js +2 -2
- package/dist/block.d.ts +4760 -0
- package/dist/block.js +104 -0
- package/dist/decorators.d.ts +8 -8
- package/dist/decorators.js +3 -43
- package/dist/index.d.ts +9 -5
- package/dist/index.js +10 -5
- package/dist/models.d.ts +115 -115
- package/dist/models.js +77 -76
- package/dist/registerTool.d.ts +68 -0
- package/dist/registerTool.js +57 -0
- package/dist/registry.d.ts +1 -1
- package/dist/registry.js +1 -1
- package/dist/service.d.ts +10 -8
- package/dist/service.js +50 -22
- package/eslint.config.js +20 -0
- package/package.json +20 -10
- package/scripts/generate-block.ts +167 -0
- package/scripts/lint.sh +7 -0
- package/src/auth.ts +21 -16
- package/src/block.ts +11761 -0
- package/src/decorators.ts +28 -67
- package/src/index.ts +9 -5
- package/src/models.ts +106 -103
- package/src/registerTool.ts +181 -0
- package/src/registry.ts +2 -2
- package/src/service.ts +80 -37
- package/tests/block.test.ts +115 -0
- package/tests/integration.test.ts +318 -0
- package/tsconfig.build.json +5 -0
- package/tsconfig.json +3 -3
- package/vitest.config.ts +7 -0
package/src/service.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import express, { Express, Request, Response, Router } from
|
|
2
|
-
|
|
3
|
-
import {
|
|
1
|
+
import express, { Express, Request, Response, Router } from "express";
|
|
2
|
+
|
|
3
|
+
import { isBlockResponse } from "./block";
|
|
4
|
+
import { AuthRequirement, Function, Parameter } from "./models";
|
|
5
|
+
import { registry } from "./registry";
|
|
4
6
|
|
|
5
7
|
export class ToolsService {
|
|
6
8
|
private app: Express;
|
|
7
|
-
private router: Router;
|
|
8
9
|
private functions: Function[] = [];
|
|
10
|
+
private router: Router;
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* Initialize a new tools service
|
|
@@ -15,22 +17,11 @@ export class ToolsService {
|
|
|
15
17
|
this.app = app;
|
|
16
18
|
this.router = express.Router();
|
|
17
19
|
this.initRoutes();
|
|
18
|
-
|
|
20
|
+
|
|
19
21
|
// Register this service in the global registry
|
|
20
22
|
registry.services.push(this);
|
|
21
23
|
}
|
|
22
24
|
|
|
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
25
|
/**
|
|
35
26
|
* Register a tool function
|
|
36
27
|
* @param name Tool name
|
|
@@ -39,23 +30,37 @@ export class ToolsService {
|
|
|
39
30
|
* @param parameters List of parameters for the tool
|
|
40
31
|
* @param endpoint API endpoint for the tool
|
|
41
32
|
* @param authRequirements Authentication requirements (optional)
|
|
33
|
+
* @param responseType Response type - 'json' (default) or 'block'
|
|
34
|
+
* @param isNewStyle Whether this is a new-style tool (registerTool) vs legacy decorator
|
|
42
35
|
*/
|
|
43
36
|
registerTool(
|
|
44
37
|
name: string,
|
|
45
38
|
description: string,
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
46
40
|
handler: any, // Changed from Function to any to avoid confusion with built-in Function type
|
|
47
41
|
parameters: Parameter[],
|
|
48
42
|
endpoint: string,
|
|
49
|
-
authRequirements?: AuthRequirement[]
|
|
43
|
+
authRequirements?: AuthRequirement[],
|
|
44
|
+
responseType: "block" | "json" = "json",
|
|
45
|
+
isNewStyle: boolean = false,
|
|
50
46
|
): void {
|
|
51
|
-
const func = new Function(
|
|
47
|
+
const func = new Function(
|
|
48
|
+
name,
|
|
49
|
+
description,
|
|
50
|
+
parameters,
|
|
51
|
+
endpoint,
|
|
52
|
+
authRequirements,
|
|
53
|
+
);
|
|
52
54
|
this.functions.push(func);
|
|
53
|
-
|
|
55
|
+
|
|
56
|
+
// Determine if this is a block tool
|
|
57
|
+
const isBlockTool = responseType === "block";
|
|
58
|
+
|
|
54
59
|
// Register the actual endpoint
|
|
55
60
|
this.router.post(endpoint, async (req: Request, res: Response) => {
|
|
56
61
|
try {
|
|
57
62
|
console.log(`Received request for ${endpoint}:`, req.body);
|
|
58
|
-
|
|
63
|
+
|
|
59
64
|
// Extract parameters from the request body
|
|
60
65
|
let params;
|
|
61
66
|
if (req.body && req.body.parameters) {
|
|
@@ -64,35 +69,73 @@ export class ToolsService {
|
|
|
64
69
|
console.log(`Extracted parameters from 'parameters' key:`, params);
|
|
65
70
|
} else {
|
|
66
71
|
// Fallback for direct testing: { "name": "value" }
|
|
67
|
-
console.log(
|
|
72
|
+
console.log(
|
|
73
|
+
`Warning: 'parameters' key not found in request body. Using body directly.`,
|
|
74
|
+
);
|
|
68
75
|
params = req.body;
|
|
69
76
|
}
|
|
70
|
-
|
|
77
|
+
|
|
71
78
|
// Extract auth data if available
|
|
72
79
|
const authData = req.body && req.body.auth;
|
|
73
80
|
if (authData) {
|
|
74
|
-
console.log(
|
|
81
|
+
console.log(
|
|
82
|
+
`Auth data provided for provider: ${authData.provider || "unknown"}`,
|
|
83
|
+
);
|
|
75
84
|
}
|
|
76
|
-
|
|
77
|
-
// Call the handler with extracted parameters
|
|
78
|
-
// Check if handler accepts auth as third parameter
|
|
79
|
-
const handlerParamCount = handler.length;
|
|
85
|
+
|
|
86
|
+
// Call the handler with extracted parameters
|
|
80
87
|
let result;
|
|
81
|
-
|
|
82
|
-
if (
|
|
83
|
-
|
|
84
|
-
|
|
88
|
+
|
|
89
|
+
if (isNewStyle) {
|
|
90
|
+
result = await handler(params, {
|
|
91
|
+
mode: (req.body && req.body.execution_mode) || "headless",
|
|
92
|
+
...(authData && { auth: authData }),
|
|
93
|
+
});
|
|
85
94
|
} else {
|
|
86
|
-
//
|
|
87
|
-
|
|
95
|
+
// Check if handler accepts auth as third parameter
|
|
96
|
+
const handlerParamCount = handler.length;
|
|
97
|
+
|
|
98
|
+
if (handlerParamCount >= 2) {
|
|
99
|
+
// Handler accepts auth data
|
|
100
|
+
result = await handler(params, authData);
|
|
101
|
+
} else {
|
|
102
|
+
// Handler doesn't accept auth data
|
|
103
|
+
result = await handler(params);
|
|
104
|
+
}
|
|
88
105
|
}
|
|
89
|
-
|
|
106
|
+
|
|
90
107
|
console.log(`Tool ${name} returned:`, result);
|
|
91
|
-
|
|
92
|
-
|
|
108
|
+
|
|
109
|
+
// Return with appropriate content-type header
|
|
110
|
+
if (isBlockTool) {
|
|
111
|
+
// Validate that block tools return a BlockResponse
|
|
112
|
+
if (!isBlockResponse(result)) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`Block tool '${name}' must return a BlockResponse object, but returned ${typeof result}`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
res.set("Content-Type", "application/vnd.opal.block+json");
|
|
118
|
+
res.json(result);
|
|
119
|
+
} else {
|
|
120
|
+
res.json(result);
|
|
121
|
+
}
|
|
122
|
+
} catch (error) {
|
|
93
123
|
console.error(`Error in tool ${name}:`, error);
|
|
94
|
-
res.status(500).json({
|
|
124
|
+
res.status(500).json({
|
|
125
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
126
|
+
});
|
|
95
127
|
}
|
|
96
128
|
});
|
|
97
129
|
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Initialize the discovery endpoint
|
|
133
|
+
*/
|
|
134
|
+
private initRoutes(): void {
|
|
135
|
+
this.router.get("/discovery", (req: Request, res: Response) => {
|
|
136
|
+
res.json({ functions: this.functions.map((f) => f.toJSON()) });
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
this.app.use(this.router);
|
|
140
|
+
}
|
|
98
141
|
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Adaptive Block components
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "vitest";
|
|
6
|
+
|
|
7
|
+
import type { BlockResponse } from "../src";
|
|
8
|
+
|
|
9
|
+
import { Block } from "../src";
|
|
10
|
+
|
|
11
|
+
describe("Block Components", () => {
|
|
12
|
+
test("Block.Heading with children", () => {
|
|
13
|
+
const heading = Block.Heading({ children: "Test Heading" });
|
|
14
|
+
expect(heading.$type).toBe("Block.Heading");
|
|
15
|
+
|
|
16
|
+
const response: BlockResponse = {
|
|
17
|
+
content: Block.Document({ children: heading }),
|
|
18
|
+
};
|
|
19
|
+
expect(response).toEqual({
|
|
20
|
+
content: {
|
|
21
|
+
$type: "Block.Document",
|
|
22
|
+
children: { $type: "Block.Heading", children: "Test Heading" },
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("Block.Text with children", () => {
|
|
28
|
+
const text = Block.Text({ children: "Test text content" });
|
|
29
|
+
expect(text.$type).toBe("Block.Text");
|
|
30
|
+
|
|
31
|
+
const response: BlockResponse = {
|
|
32
|
+
content: Block.Document({ children: text }),
|
|
33
|
+
};
|
|
34
|
+
expect(response).toEqual({
|
|
35
|
+
content: {
|
|
36
|
+
$type: "Block.Document",
|
|
37
|
+
children: { $type: "Block.Text", children: "Test text content" },
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("Block.Heading with additional properties", () => {
|
|
43
|
+
const heading = Block.Heading({
|
|
44
|
+
children: "Styled Heading",
|
|
45
|
+
fontSize: "md",
|
|
46
|
+
fontWeight: "600",
|
|
47
|
+
level: "2",
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const response: BlockResponse = {
|
|
51
|
+
content: Block.Document({ children: heading }),
|
|
52
|
+
};
|
|
53
|
+
expect(response).toEqual({
|
|
54
|
+
content: {
|
|
55
|
+
$type: "Block.Document",
|
|
56
|
+
children: {
|
|
57
|
+
$type: "Block.Heading",
|
|
58
|
+
children: "Styled Heading",
|
|
59
|
+
fontSize: "md",
|
|
60
|
+
fontWeight: "600",
|
|
61
|
+
level: "2",
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("Block.Group with children array", () => {
|
|
68
|
+
const group = Block.Group({
|
|
69
|
+
children: [
|
|
70
|
+
Block.Text({ children: "First item" }),
|
|
71
|
+
Block.Text({ children: "Second item" }),
|
|
72
|
+
],
|
|
73
|
+
flexDirection: "column",
|
|
74
|
+
gap: "16",
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const response: BlockResponse = {
|
|
78
|
+
content: Block.Document({ children: group }),
|
|
79
|
+
};
|
|
80
|
+
expect(response).toEqual({
|
|
81
|
+
content: {
|
|
82
|
+
$type: "Block.Document",
|
|
83
|
+
children: {
|
|
84
|
+
$type: "Block.Group",
|
|
85
|
+
children: [
|
|
86
|
+
{ $type: "Block.Text", children: "First item" },
|
|
87
|
+
{ $type: "Block.Text", children: "Second item" },
|
|
88
|
+
],
|
|
89
|
+
flexDirection: "column",
|
|
90
|
+
gap: "16",
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("Block.Document", () => {
|
|
97
|
+
const doc = Block.Document({
|
|
98
|
+
children: [
|
|
99
|
+
Block.Heading({ children: "Title", level: "2" }),
|
|
100
|
+
Block.Text({ children: "Description" }),
|
|
101
|
+
],
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const response: BlockResponse = { content: doc };
|
|
105
|
+
expect(response).toEqual({
|
|
106
|
+
content: {
|
|
107
|
+
$type: "Block.Document",
|
|
108
|
+
children: [
|
|
109
|
+
{ $type: "Block.Heading", children: "Title", level: "2" },
|
|
110
|
+
{ $type: "Block.Text", children: "Description" },
|
|
111
|
+
],
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Express integration with the registerTool() function
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import express from "express";
|
|
6
|
+
import request from "supertest";
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
|
8
|
+
import { z } from "zod/v4";
|
|
9
|
+
|
|
10
|
+
import { Block, ParameterType, registerTool, tool, ToolsService } from "../src";
|
|
11
|
+
import { registry } from "../src/registry";
|
|
12
|
+
|
|
13
|
+
// Clear registry before each test
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
registry.services = [];
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
registry.services = [];
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe("Integration Tests", () => {
|
|
23
|
+
test("simple tool endpoint", async () => {
|
|
24
|
+
const app = express();
|
|
25
|
+
app.use(express.json());
|
|
26
|
+
new ToolsService(app);
|
|
27
|
+
|
|
28
|
+
registerTool(
|
|
29
|
+
"greet",
|
|
30
|
+
{
|
|
31
|
+
description: "Greet a user",
|
|
32
|
+
inputSchema: {
|
|
33
|
+
name: z.string().describe("The name to greet"),
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
async (params) => {
|
|
37
|
+
return `Hello, ${params.name}!`;
|
|
38
|
+
},
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
// Call the endpoint
|
|
42
|
+
const response = await request(app)
|
|
43
|
+
.post("/tools/greet")
|
|
44
|
+
.send({ parameters: { name: "Alice" } });
|
|
45
|
+
|
|
46
|
+
expect(response.status).toBe(200);
|
|
47
|
+
// Verify correct content-type header for regular tools
|
|
48
|
+
expect(response.headers["content-type"]).toMatch(/application\/json/);
|
|
49
|
+
expect(response.body).toBe("Hello, Alice!");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("block tool endpoint correctly serializes all fields", async () => {
|
|
53
|
+
const app = express();
|
|
54
|
+
app.use(express.json());
|
|
55
|
+
new ToolsService(app);
|
|
56
|
+
|
|
57
|
+
registerTool(
|
|
58
|
+
"create_task",
|
|
59
|
+
{
|
|
60
|
+
description: "Create a task",
|
|
61
|
+
inputSchema: {
|
|
62
|
+
name: z.string().describe("Task name"),
|
|
63
|
+
},
|
|
64
|
+
type: "block",
|
|
65
|
+
},
|
|
66
|
+
async (params) => {
|
|
67
|
+
return {
|
|
68
|
+
artifact: {
|
|
69
|
+
data: { name: params.name },
|
|
70
|
+
id: "task-123",
|
|
71
|
+
type: "task",
|
|
72
|
+
},
|
|
73
|
+
content: Block.Document({
|
|
74
|
+
children: Block.Text({ children: `Created task: ${params.name}` }),
|
|
75
|
+
}),
|
|
76
|
+
data: { created_at: "2024-01-01T00:00:00Z" },
|
|
77
|
+
rollback: {
|
|
78
|
+
config: { endpoint: "/api/tasks/task-123", method: "DELETE" },
|
|
79
|
+
label: "Delete Task",
|
|
80
|
+
type: "endpoint",
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const response = await request(app)
|
|
87
|
+
.post("/tools/create-task")
|
|
88
|
+
.send({ parameters: { name: "My Task" } });
|
|
89
|
+
|
|
90
|
+
expect(response.status).toBe(200);
|
|
91
|
+
expect(response.headers["content-type"]).toBe(
|
|
92
|
+
"application/vnd.opal.block+json; charset=utf-8",
|
|
93
|
+
);
|
|
94
|
+
expect(response.body).toEqual({
|
|
95
|
+
artifact: { data: { name: "My Task" }, id: "task-123", type: "task" },
|
|
96
|
+
content: {
|
|
97
|
+
$type: "Block.Document",
|
|
98
|
+
children: { $type: "Block.Text", children: "Created task: My Task" },
|
|
99
|
+
},
|
|
100
|
+
data: { created_at: "2024-01-01T00:00:00Z" },
|
|
101
|
+
rollback: {
|
|
102
|
+
config: { endpoint: "/api/tasks/task-123", method: "DELETE" },
|
|
103
|
+
label: "Delete Task",
|
|
104
|
+
type: "endpoint",
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("environment parameter is correctly passed to tool", async () => {
|
|
110
|
+
const app = express();
|
|
111
|
+
app.use(express.json());
|
|
112
|
+
new ToolsService(app);
|
|
113
|
+
|
|
114
|
+
registerTool(
|
|
115
|
+
"check_env",
|
|
116
|
+
{
|
|
117
|
+
description: "Check environment",
|
|
118
|
+
inputSchema: {
|
|
119
|
+
name: z.string().describe("Name parameter"),
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
async (params, extra) => {
|
|
123
|
+
return {
|
|
124
|
+
has_environment: extra !== undefined,
|
|
125
|
+
param_name: params.name,
|
|
126
|
+
};
|
|
127
|
+
},
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const response = await request(app)
|
|
131
|
+
.post("/tools/check-env")
|
|
132
|
+
.send({ parameters: { name: "Test" } });
|
|
133
|
+
|
|
134
|
+
expect(response.status).toBe(200);
|
|
135
|
+
expect(response.body).toEqual({
|
|
136
|
+
has_environment: true,
|
|
137
|
+
param_name: "Test",
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("invalid parameters return proper error", async () => {
|
|
142
|
+
const app = express();
|
|
143
|
+
app.use(express.json());
|
|
144
|
+
new ToolsService(app);
|
|
145
|
+
|
|
146
|
+
registerTool(
|
|
147
|
+
"validate_params",
|
|
148
|
+
{
|
|
149
|
+
description: "Validate parameters",
|
|
150
|
+
inputSchema: {
|
|
151
|
+
name: z.string().describe("Required name"),
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
async (params) => {
|
|
155
|
+
if (!params.name) {
|
|
156
|
+
throw new Error("Name is required");
|
|
157
|
+
}
|
|
158
|
+
return `Valid: ${params.name}`;
|
|
159
|
+
},
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
// Call with missing required parameter
|
|
163
|
+
const response = await request(app)
|
|
164
|
+
.post("/tools/validate-params")
|
|
165
|
+
.send({ parameters: {} });
|
|
166
|
+
|
|
167
|
+
expect(response.status).toBe(500);
|
|
168
|
+
expect(response.body).toHaveProperty("error");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("multiple tools on same service and discovery endpoint", async () => {
|
|
172
|
+
const app = express();
|
|
173
|
+
app.use(express.json());
|
|
174
|
+
new ToolsService(app);
|
|
175
|
+
|
|
176
|
+
// Use legacy @tool decorator for first tool by calling it as a function
|
|
177
|
+
const getWeatherHandler = async () => {
|
|
178
|
+
return { condition: "sunny", temperature: 22 };
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
tool({
|
|
182
|
+
description: "Gets current weather for a location",
|
|
183
|
+
name: "get_weather",
|
|
184
|
+
parameters: [
|
|
185
|
+
{
|
|
186
|
+
description: "City name or location",
|
|
187
|
+
name: "location",
|
|
188
|
+
required: true,
|
|
189
|
+
type: ParameterType.String,
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
description: "Temperature units",
|
|
193
|
+
name: "units",
|
|
194
|
+
required: false,
|
|
195
|
+
type: ParameterType.String,
|
|
196
|
+
},
|
|
197
|
+
],
|
|
198
|
+
})(getWeatherHandler);
|
|
199
|
+
|
|
200
|
+
// Use modern registerTool for second tool
|
|
201
|
+
registerTool(
|
|
202
|
+
"create_task",
|
|
203
|
+
{
|
|
204
|
+
authRequirements: {
|
|
205
|
+
provider: "google",
|
|
206
|
+
required: true,
|
|
207
|
+
scopeBundle: "tasks",
|
|
208
|
+
},
|
|
209
|
+
description: "Create a new task",
|
|
210
|
+
inputSchema: {
|
|
211
|
+
title: z.string().describe("Task title"),
|
|
212
|
+
},
|
|
213
|
+
type: "block",
|
|
214
|
+
},
|
|
215
|
+
async (params) => {
|
|
216
|
+
return {
|
|
217
|
+
content: Block.Document({
|
|
218
|
+
children: Block.Text({ children: `Task: ${params.title}` }),
|
|
219
|
+
}),
|
|
220
|
+
};
|
|
221
|
+
},
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
// Test both tool endpoints work
|
|
225
|
+
const weatherResponse = await request(app)
|
|
226
|
+
.post("/tools/get-weather")
|
|
227
|
+
.send({ parameters: { location: "San Francisco" } });
|
|
228
|
+
const taskResponse = await request(app)
|
|
229
|
+
.post("/tools/create-task")
|
|
230
|
+
.send({ parameters: { title: "My Task" } });
|
|
231
|
+
|
|
232
|
+
expect(weatherResponse.status).toBe(200);
|
|
233
|
+
expect(taskResponse.status).toBe(200);
|
|
234
|
+
|
|
235
|
+
// Test discovery endpoint
|
|
236
|
+
const discoveryResponse = await request(app).get("/discovery");
|
|
237
|
+
|
|
238
|
+
expect(discoveryResponse.status).toBe(200);
|
|
239
|
+
expect(discoveryResponse.headers["content-type"]).toMatch(
|
|
240
|
+
/application\/json/,
|
|
241
|
+
);
|
|
242
|
+
expect(discoveryResponse.body).toEqual({
|
|
243
|
+
functions: [
|
|
244
|
+
{
|
|
245
|
+
description: "Gets current weather for a location",
|
|
246
|
+
endpoint: "/tools/get-weather",
|
|
247
|
+
http_method: "POST",
|
|
248
|
+
name: "get_weather",
|
|
249
|
+
parameters: [
|
|
250
|
+
{
|
|
251
|
+
description: "City name or location",
|
|
252
|
+
name: "location",
|
|
253
|
+
required: true,
|
|
254
|
+
type: "string",
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
description: "Temperature units",
|
|
258
|
+
name: "units",
|
|
259
|
+
required: false,
|
|
260
|
+
type: "string",
|
|
261
|
+
},
|
|
262
|
+
],
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
auth_requirements: [
|
|
266
|
+
{
|
|
267
|
+
provider: "google",
|
|
268
|
+
required: true,
|
|
269
|
+
scope_bundle: "tasks",
|
|
270
|
+
},
|
|
271
|
+
],
|
|
272
|
+
description: "Create a new task",
|
|
273
|
+
endpoint: "/tools/create-task",
|
|
274
|
+
http_method: "POST",
|
|
275
|
+
name: "create_task",
|
|
276
|
+
parameters: [
|
|
277
|
+
{
|
|
278
|
+
description: "Task title",
|
|
279
|
+
name: "title",
|
|
280
|
+
required: true,
|
|
281
|
+
type: "string",
|
|
282
|
+
},
|
|
283
|
+
],
|
|
284
|
+
},
|
|
285
|
+
],
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test("block tool without BlockResponse throws error", async () => {
|
|
290
|
+
const app = express();
|
|
291
|
+
app.use(express.json());
|
|
292
|
+
new ToolsService(app);
|
|
293
|
+
|
|
294
|
+
// @ts-expect-error - Testing runtime validation of invalid block tool return type
|
|
295
|
+
registerTool(
|
|
296
|
+
"invalid_block",
|
|
297
|
+
{
|
|
298
|
+
description: "Block tool that returns wrong type",
|
|
299
|
+
inputSchema: {
|
|
300
|
+
name: z.string().describe("Name"),
|
|
301
|
+
},
|
|
302
|
+
type: "block",
|
|
303
|
+
},
|
|
304
|
+
async (params) => {
|
|
305
|
+
// This should fail - block tools must return BlockResponse
|
|
306
|
+
return `This should fail: ${params.name}`;
|
|
307
|
+
},
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
const response = await request(app)
|
|
311
|
+
.post("/tools/invalid-block")
|
|
312
|
+
.send({ parameters: { name: "Test" } });
|
|
313
|
+
|
|
314
|
+
// Should return 500 error because block tools must return BlockResponse
|
|
315
|
+
expect(response.status).toBe(500);
|
|
316
|
+
expect(response.body.error).toContain("must return a BlockResponse object");
|
|
317
|
+
});
|
|
318
|
+
});
|
package/tsconfig.json
CHANGED