@vinkius-core/mcp-fusion 0.1.0
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/LICENSE +190 -0
- package/README.md +391 -0
- package/dist/src/AbstractBase.d.ts +24 -0
- package/dist/src/AbstractBase.d.ts.map +1 -0
- package/dist/src/AbstractBase.js +63 -0
- package/dist/src/AbstractBase.js.map +1 -0
- package/dist/src/AbstractLeaf.d.ts +12 -0
- package/dist/src/AbstractLeaf.d.ts.map +1 -0
- package/dist/src/AbstractLeaf.js +32 -0
- package/dist/src/AbstractLeaf.js.map +1 -0
- package/dist/src/Annotations.d.ts +15 -0
- package/dist/src/Annotations.d.ts.map +1 -0
- package/dist/src/Annotations.js +29 -0
- package/dist/src/Annotations.js.map +1 -0
- package/dist/src/Group.d.ts +32 -0
- package/dist/src/Group.d.ts.map +1 -0
- package/dist/src/Group.js +131 -0
- package/dist/src/Group.js.map +1 -0
- package/dist/src/Icon.d.ts +19 -0
- package/dist/src/Icon.d.ts.map +1 -0
- package/dist/src/Icon.js +33 -0
- package/dist/src/Icon.js.map +1 -0
- package/dist/src/Prompt.d.ts +11 -0
- package/dist/src/Prompt.d.ts.map +1 -0
- package/dist/src/Prompt.js +28 -0
- package/dist/src/Prompt.js.map +1 -0
- package/dist/src/PromptArgument.d.ts +10 -0
- package/dist/src/PromptArgument.d.ts.map +1 -0
- package/dist/src/PromptArgument.js +20 -0
- package/dist/src/PromptArgument.js.map +1 -0
- package/dist/src/Resource.d.ts +19 -0
- package/dist/src/Resource.d.ts.map +1 -0
- package/dist/src/Resource.js +34 -0
- package/dist/src/Resource.js.map +1 -0
- package/dist/src/Role.d.ts +5 -0
- package/dist/src/Role.d.ts.map +1 -0
- package/dist/src/Role.js +6 -0
- package/dist/src/Role.js.map +1 -0
- package/dist/src/Tool.d.ts +16 -0
- package/dist/src/Tool.d.ts.map +1 -0
- package/dist/src/Tool.js +28 -0
- package/dist/src/Tool.js.map +1 -0
- package/dist/src/ToolAnnotations.d.ts +23 -0
- package/dist/src/ToolAnnotations.d.ts.map +1 -0
- package/dist/src/ToolAnnotations.js +44 -0
- package/dist/src/ToolAnnotations.js.map +1 -0
- package/dist/src/converters/GroupConverter.d.ts +14 -0
- package/dist/src/converters/GroupConverter.d.ts.map +1 -0
- package/dist/src/converters/GroupConverter.js +13 -0
- package/dist/src/converters/GroupConverter.js.map +1 -0
- package/dist/src/converters/PromptConverter.d.ts +14 -0
- package/dist/src/converters/PromptConverter.d.ts.map +1 -0
- package/dist/src/converters/PromptConverter.js +13 -0
- package/dist/src/converters/PromptConverter.js.map +1 -0
- package/dist/src/converters/ResourceConverter.d.ts +14 -0
- package/dist/src/converters/ResourceConverter.d.ts.map +1 -0
- package/dist/src/converters/ResourceConverter.js +13 -0
- package/dist/src/converters/ResourceConverter.js.map +1 -0
- package/dist/src/converters/ToolAnnotationsConverter.d.ts +16 -0
- package/dist/src/converters/ToolAnnotationsConverter.d.ts.map +1 -0
- package/dist/src/converters/ToolAnnotationsConverter.js +23 -0
- package/dist/src/converters/ToolAnnotationsConverter.js.map +1 -0
- package/dist/src/converters/ToolConverter.d.ts +14 -0
- package/dist/src/converters/ToolConverter.d.ts.map +1 -0
- package/dist/src/converters/ToolConverter.js +13 -0
- package/dist/src/converters/ToolConverter.js.map +1 -0
- package/dist/src/converters/index.d.ts +6 -0
- package/dist/src/converters/index.d.ts.map +1 -0
- package/dist/src/converters/index.js +6 -0
- package/dist/src/converters/index.js.map +1 -0
- package/dist/src/framework/GroupedToolBuilder.d.ts +137 -0
- package/dist/src/framework/GroupedToolBuilder.d.ts.map +1 -0
- package/dist/src/framework/GroupedToolBuilder.js +289 -0
- package/dist/src/framework/GroupedToolBuilder.js.map +1 -0
- package/dist/src/framework/ResponseHelper.d.ts +43 -0
- package/dist/src/framework/ResponseHelper.d.ts.map +1 -0
- package/dist/src/framework/ResponseHelper.js +49 -0
- package/dist/src/framework/ResponseHelper.js.map +1 -0
- package/dist/src/framework/ToolBuilder.d.ts +46 -0
- package/dist/src/framework/ToolBuilder.d.ts.map +1 -0
- package/dist/src/framework/ToolBuilder.js +2 -0
- package/dist/src/framework/ToolBuilder.js.map +1 -0
- package/dist/src/framework/ToolRegistry.d.ts +85 -0
- package/dist/src/framework/ToolRegistry.d.ts.map +1 -0
- package/dist/src/framework/ToolRegistry.js +153 -0
- package/dist/src/framework/ToolRegistry.js.map +1 -0
- package/dist/src/framework/index.d.ts +9 -0
- package/dist/src/framework/index.d.ts.map +1 -0
- package/dist/src/framework/index.js +8 -0
- package/dist/src/framework/index.js.map +1 -0
- package/dist/src/framework/strategies/AnnotationAggregator.d.ts +11 -0
- package/dist/src/framework/strategies/AnnotationAggregator.d.ts.map +1 -0
- package/dist/src/framework/strategies/AnnotationAggregator.js +25 -0
- package/dist/src/framework/strategies/AnnotationAggregator.js.map +1 -0
- package/dist/src/framework/strategies/DescriptionGenerator.d.ts +12 -0
- package/dist/src/framework/strategies/DescriptionGenerator.d.ts.map +1 -0
- package/dist/src/framework/strategies/DescriptionGenerator.js +70 -0
- package/dist/src/framework/strategies/DescriptionGenerator.js.map +1 -0
- package/dist/src/framework/strategies/MiddlewareCompiler.d.ts +13 -0
- package/dist/src/framework/strategies/MiddlewareCompiler.d.ts.map +1 -0
- package/dist/src/framework/strategies/MiddlewareCompiler.js +24 -0
- package/dist/src/framework/strategies/MiddlewareCompiler.js.map +1 -0
- package/dist/src/framework/strategies/SchemaGenerator.d.ts +15 -0
- package/dist/src/framework/strategies/SchemaGenerator.d.ts.map +1 -0
- package/dist/src/framework/strategies/SchemaGenerator.js +97 -0
- package/dist/src/framework/strategies/SchemaGenerator.js.map +1 -0
- package/dist/src/framework/strategies/SchemaUtils.d.ts +7 -0
- package/dist/src/framework/strategies/SchemaUtils.d.ts.map +1 -0
- package/dist/src/framework/strategies/SchemaUtils.js +17 -0
- package/dist/src/framework/strategies/SchemaUtils.js.map +1 -0
- package/dist/src/framework/strategies/ToonDescriptionGenerator.d.ts +3 -0
- package/dist/src/framework/strategies/ToonDescriptionGenerator.d.ts.map +1 -0
- package/dist/src/framework/strategies/ToonDescriptionGenerator.js +53 -0
- package/dist/src/framework/strategies/ToonDescriptionGenerator.js.map +1 -0
- package/dist/src/framework/strategies/Types.d.ts +34 -0
- package/dist/src/framework/strategies/Types.d.ts.map +1 -0
- package/dist/src/framework/strategies/Types.js +2 -0
- package/dist/src/framework/strategies/Types.js.map +1 -0
- package/dist/src/framework/strategies/index.d.ts +12 -0
- package/dist/src/framework/strategies/index.d.ts.map +1 -0
- package/dist/src/framework/strategies/index.js +11 -0
- package/dist/src/framework/strategies/index.js.map +1 -0
- package/dist/src/index.d.ts +15 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +15 -0
- package/dist/src/index.js.map +1 -0
- package/dist/tests/AbstractBase.test.d.ts +2 -0
- package/dist/tests/AbstractBase.test.d.ts.map +1 -0
- package/dist/tests/AbstractBase.test.js +130 -0
- package/dist/tests/AbstractBase.test.js.map +1 -0
- package/dist/tests/AbstractLeaf.test.d.ts +2 -0
- package/dist/tests/AbstractLeaf.test.d.ts.map +1 -0
- package/dist/tests/AbstractLeaf.test.js +65 -0
- package/dist/tests/AbstractLeaf.test.js.map +1 -0
- package/dist/tests/Annotations.test.d.ts +2 -0
- package/dist/tests/Annotations.test.d.ts.map +1 -0
- package/dist/tests/Annotations.test.js +34 -0
- package/dist/tests/Annotations.test.js.map +1 -0
- package/dist/tests/BarrelExport.test.d.ts +2 -0
- package/dist/tests/BarrelExport.test.d.ts.map +1 -0
- package/dist/tests/BarrelExport.test.js +42 -0
- package/dist/tests/BarrelExport.test.js.map +1 -0
- package/dist/tests/Converters.test.d.ts +2 -0
- package/dist/tests/Converters.test.d.ts.map +1 -0
- package/dist/tests/Converters.test.js +193 -0
- package/dist/tests/Converters.test.js.map +1 -0
- package/dist/tests/Group.test.d.ts +2 -0
- package/dist/tests/Group.test.d.ts.map +1 -0
- package/dist/tests/Group.test.js +257 -0
- package/dist/tests/Group.test.js.map +1 -0
- package/dist/tests/Icon.test.d.ts +2 -0
- package/dist/tests/Icon.test.d.ts.map +1 -0
- package/dist/tests/Icon.test.js +44 -0
- package/dist/tests/Icon.test.js.map +1 -0
- package/dist/tests/Prompt.test.d.ts +2 -0
- package/dist/tests/Prompt.test.d.ts.map +1 -0
- package/dist/tests/Prompt.test.js +63 -0
- package/dist/tests/Prompt.test.js.map +1 -0
- package/dist/tests/PromptArgument.test.d.ts +2 -0
- package/dist/tests/PromptArgument.test.d.ts.map +1 -0
- package/dist/tests/PromptArgument.test.js +37 -0
- package/dist/tests/PromptArgument.test.js.map +1 -0
- package/dist/tests/Resource.test.d.ts +2 -0
- package/dist/tests/Resource.test.d.ts.map +1 -0
- package/dist/tests/Resource.test.js +61 -0
- package/dist/tests/Resource.test.js.map +1 -0
- package/dist/tests/Role.test.d.ts +2 -0
- package/dist/tests/Role.test.d.ts.map +1 -0
- package/dist/tests/Role.test.js +17 -0
- package/dist/tests/Role.test.js.map +1 -0
- package/dist/tests/Tool.test.d.ts +2 -0
- package/dist/tests/Tool.test.d.ts.map +1 -0
- package/dist/tests/Tool.test.js +62 -0
- package/dist/tests/Tool.test.js.map +1 -0
- package/dist/tests/ToolAnnotations.test.d.ts +2 -0
- package/dist/tests/ToolAnnotations.test.d.ts.map +1 -0
- package/dist/tests/ToolAnnotations.test.js +55 -0
- package/dist/tests/ToolAnnotations.test.js.map +1 -0
- package/dist/tests/framework/AdversarialQA.test.d.ts +2 -0
- package/dist/tests/framework/AdversarialQA.test.d.ts.map +1 -0
- package/dist/tests/framework/AdversarialQA.test.js +906 -0
- package/dist/tests/framework/AdversarialQA.test.js.map +1 -0
- package/dist/tests/framework/GroupedToolBuilder.test.d.ts +2 -0
- package/dist/tests/framework/GroupedToolBuilder.test.d.ts.map +1 -0
- package/dist/tests/framework/GroupedToolBuilder.test.js +712 -0
- package/dist/tests/framework/GroupedToolBuilder.test.js.map +1 -0
- package/dist/tests/framework/LargeScaleScenarios.test.d.ts +2 -0
- package/dist/tests/framework/LargeScaleScenarios.test.d.ts.map +1 -0
- package/dist/tests/framework/LargeScaleScenarios.test.js +1043 -0
- package/dist/tests/framework/LargeScaleScenarios.test.js.map +1 -0
- package/dist/tests/framework/McpServerAdapter.test.d.ts +2 -0
- package/dist/tests/framework/McpServerAdapter.test.d.ts.map +1 -0
- package/dist/tests/framework/McpServerAdapter.test.js +380 -0
- package/dist/tests/framework/McpServerAdapter.test.js.map +1 -0
- package/dist/tests/framework/ResponseHelper.test.d.ts +2 -0
- package/dist/tests/framework/ResponseHelper.test.d.ts.map +1 -0
- package/dist/tests/framework/ResponseHelper.test.js +202 -0
- package/dist/tests/framework/ResponseHelper.test.js.map +1 -0
- package/dist/tests/framework/SecurityDeep.test.d.ts +2 -0
- package/dist/tests/framework/SecurityDeep.test.d.ts.map +1 -0
- package/dist/tests/framework/SecurityDeep.test.js +825 -0
- package/dist/tests/framework/SecurityDeep.test.js.map +1 -0
- package/dist/tests/framework/ToolRegistry.test.d.ts +2 -0
- package/dist/tests/framework/ToolRegistry.test.d.ts.map +1 -0
- package/dist/tests/framework/ToolRegistry.test.js +152 -0
- package/dist/tests/framework/ToolRegistry.test.js.map +1 -0
- package/dist/tests/framework/ToonDescription.test.d.ts +2 -0
- package/dist/tests/framework/ToonDescription.test.d.ts.map +1 -0
- package/dist/tests/framework/ToonDescription.test.js +287 -0
- package/dist/tests/framework/ToonDescription.test.js.map +1 -0
- package/package.json +64 -0
|
@@ -0,0 +1,712 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { GroupedToolBuilder, success, error } from '../../src/framework/index.js';
|
|
4
|
+
// ============================================================================
|
|
5
|
+
// Helper: dummy handler
|
|
6
|
+
// ============================================================================
|
|
7
|
+
const dummyHandler = async (_ctx, _args) => success('ok');
|
|
8
|
+
const echoHandler = async (_ctx, args) => success(JSON.stringify(args));
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Flat Mode
|
|
11
|
+
// ============================================================================
|
|
12
|
+
describe('GroupedToolBuilder — Flat Mode', () => {
|
|
13
|
+
it('should generate correct enum for flat actions', () => {
|
|
14
|
+
const builder = new GroupedToolBuilder('label')
|
|
15
|
+
.description('Label management')
|
|
16
|
+
.action({ name: 'list', handler: dummyHandler })
|
|
17
|
+
.action({ name: 'create', handler: dummyHandler })
|
|
18
|
+
.action({ name: 'delete', handler: dummyHandler });
|
|
19
|
+
const tool = builder.buildToolDefinition();
|
|
20
|
+
expect(tool.name).toBe('label');
|
|
21
|
+
const actionProp = tool.inputSchema.properties.action;
|
|
22
|
+
expect(actionProp.enum).toEqual(['list', 'create', 'delete']);
|
|
23
|
+
});
|
|
24
|
+
it('should auto-generate description with action list', () => {
|
|
25
|
+
const builder = new GroupedToolBuilder('label')
|
|
26
|
+
.description('Label management')
|
|
27
|
+
.action({ name: 'list', handler: dummyHandler })
|
|
28
|
+
.action({ name: 'create', handler: dummyHandler });
|
|
29
|
+
const tool = builder.buildToolDefinition();
|
|
30
|
+
expect(tool.description).toContain('Label management');
|
|
31
|
+
expect(tool.description).toContain('Actions: list, create');
|
|
32
|
+
});
|
|
33
|
+
it('should include workflow lines for actions with required params', () => {
|
|
34
|
+
const builder = new GroupedToolBuilder('label')
|
|
35
|
+
.description('Labels')
|
|
36
|
+
.action({
|
|
37
|
+
name: 'list',
|
|
38
|
+
handler: dummyHandler,
|
|
39
|
+
})
|
|
40
|
+
.action({
|
|
41
|
+
name: 'create',
|
|
42
|
+
description: 'Create a new label',
|
|
43
|
+
schema: z.object({
|
|
44
|
+
title: z.string(),
|
|
45
|
+
color: z.string(),
|
|
46
|
+
}),
|
|
47
|
+
handler: dummyHandler,
|
|
48
|
+
});
|
|
49
|
+
const tool = builder.buildToolDefinition();
|
|
50
|
+
expect(tool.description).toContain("- 'create': Create a new label. Requires: title, color");
|
|
51
|
+
// 'list' has no required params → no workflow line
|
|
52
|
+
expect(tool.description).not.toContain("- 'list'");
|
|
53
|
+
});
|
|
54
|
+
it('should reject action names containing dots', () => {
|
|
55
|
+
const builder = new GroupedToolBuilder('test');
|
|
56
|
+
expect(() => {
|
|
57
|
+
builder.action({ name: 'v2.list', handler: dummyHandler });
|
|
58
|
+
}).toThrow('must not contain dots');
|
|
59
|
+
});
|
|
60
|
+
it('should reject empty builder (no actions)', () => {
|
|
61
|
+
const builder = new GroupedToolBuilder('empty');
|
|
62
|
+
expect(() => builder.buildToolDefinition()).toThrow('no actions registered');
|
|
63
|
+
});
|
|
64
|
+
it('should store tags correctly', () => {
|
|
65
|
+
const builder = new GroupedToolBuilder('test')
|
|
66
|
+
.tags('authenticated', 'project-context')
|
|
67
|
+
.action({ name: 'list', handler: dummyHandler });
|
|
68
|
+
expect(builder.getTags()).toEqual(['authenticated', 'project-context']);
|
|
69
|
+
});
|
|
70
|
+
it('should support custom discriminator', () => {
|
|
71
|
+
const builder = new GroupedToolBuilder('report')
|
|
72
|
+
.discriminator('method')
|
|
73
|
+
.action({ name: 'generate', handler: dummyHandler });
|
|
74
|
+
const tool = builder.buildToolDefinition();
|
|
75
|
+
expect(tool.inputSchema.properties.method).toBeDefined();
|
|
76
|
+
expect(tool.inputSchema.properties.action).toBeUndefined();
|
|
77
|
+
expect(tool.inputSchema.required).toContain('method');
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
// ============================================================================
|
|
81
|
+
// Group Mode
|
|
82
|
+
// ============================================================================
|
|
83
|
+
describe('GroupedToolBuilder — Group Mode', () => {
|
|
84
|
+
it('should generate compound enum for grouped actions', () => {
|
|
85
|
+
const builder = new GroupedToolBuilder('project')
|
|
86
|
+
.description('Project management')
|
|
87
|
+
.group('core', 'Core operations', g => g
|
|
88
|
+
.action({ name: 'list', handler: dummyHandler })
|
|
89
|
+
.action({ name: 'create', handler: dummyHandler }))
|
|
90
|
+
.group('team', 'Team management', g => g
|
|
91
|
+
.action({ name: 'add', handler: dummyHandler }));
|
|
92
|
+
const tool = builder.buildToolDefinition();
|
|
93
|
+
const actionProp = tool.inputSchema.properties.action;
|
|
94
|
+
expect(actionProp.enum).toEqual(['core.list', 'core.create', 'team.add']);
|
|
95
|
+
});
|
|
96
|
+
it('should generate modules-style description', () => {
|
|
97
|
+
const builder = new GroupedToolBuilder('project')
|
|
98
|
+
.description('Project management')
|
|
99
|
+
.group('core', 'Core', g => g
|
|
100
|
+
.action({ name: 'list', handler: dummyHandler })
|
|
101
|
+
.action({ name: 'get', handler: dummyHandler }))
|
|
102
|
+
.group('team', 'Team', g => g
|
|
103
|
+
.action({ name: 'members', handler: dummyHandler })
|
|
104
|
+
.action({ name: 'add', handler: dummyHandler }));
|
|
105
|
+
const tool = builder.buildToolDefinition();
|
|
106
|
+
expect(tool.description).toContain('Modules: core (list,get) | team (members,add)');
|
|
107
|
+
});
|
|
108
|
+
it('should reject mixed flat + group usage', () => {
|
|
109
|
+
const builder = new GroupedToolBuilder('mixed')
|
|
110
|
+
.action({ name: 'list', handler: dummyHandler });
|
|
111
|
+
expect(() => {
|
|
112
|
+
builder.group('core', 'Core', g => g
|
|
113
|
+
.action({ name: 'get', handler: dummyHandler }));
|
|
114
|
+
}).toThrow('Cannot use .group() and .action()');
|
|
115
|
+
});
|
|
116
|
+
it('should reject group names with dots', () => {
|
|
117
|
+
const builder = new GroupedToolBuilder('test');
|
|
118
|
+
expect(() => {
|
|
119
|
+
builder.group('v2.core', 'Core', g => g
|
|
120
|
+
.action({ name: 'list', handler: dummyHandler }));
|
|
121
|
+
}).toThrow('must not contain dots');
|
|
122
|
+
});
|
|
123
|
+
it('should reject action names with dots inside a group builder', () => {
|
|
124
|
+
const builder = new GroupedToolBuilder('test');
|
|
125
|
+
expect(() => {
|
|
126
|
+
builder.group('core', 'Core', g => g
|
|
127
|
+
.action({ name: 'v2.list', handler: dummyHandler }));
|
|
128
|
+
}).toThrow('must not contain dots');
|
|
129
|
+
});
|
|
130
|
+
it('should reject flat .action() after .group() was used', () => {
|
|
131
|
+
const builder = new GroupedToolBuilder('mixed')
|
|
132
|
+
.group('core', 'Core', g => g
|
|
133
|
+
.action({ name: 'list', handler: dummyHandler }));
|
|
134
|
+
expect(() => {
|
|
135
|
+
builder.action({ name: 'create', handler: dummyHandler });
|
|
136
|
+
}).toThrow('Cannot use .action() and .group()');
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
// ============================================================================
|
|
140
|
+
// Defense Chain (Zod Validation)
|
|
141
|
+
// ============================================================================
|
|
142
|
+
describe('GroupedToolBuilder — Defense Chain', () => {
|
|
143
|
+
it('should validate types via Zod and return error on wrong type', async () => {
|
|
144
|
+
const builder = new GroupedToolBuilder('test')
|
|
145
|
+
.action({
|
|
146
|
+
name: 'create',
|
|
147
|
+
schema: z.object({ title: z.string() }),
|
|
148
|
+
handler: dummyHandler,
|
|
149
|
+
});
|
|
150
|
+
builder.buildToolDefinition();
|
|
151
|
+
const result = await builder.execute(undefined, {
|
|
152
|
+
action: 'create',
|
|
153
|
+
title: 123, // wrong type
|
|
154
|
+
});
|
|
155
|
+
expect(result.isError).toBe(true);
|
|
156
|
+
expect(result.content[0].text).toContain('Validation failed');
|
|
157
|
+
expect(result.content[0].text).toContain('title');
|
|
158
|
+
});
|
|
159
|
+
it('should strip unknown fields', async () => {
|
|
160
|
+
const builder = new GroupedToolBuilder('test')
|
|
161
|
+
.action({
|
|
162
|
+
name: 'create',
|
|
163
|
+
schema: z.object({ title: z.string() }),
|
|
164
|
+
handler: echoHandler,
|
|
165
|
+
});
|
|
166
|
+
builder.buildToolDefinition();
|
|
167
|
+
const result = await builder.execute(undefined, {
|
|
168
|
+
action: 'create',
|
|
169
|
+
title: 'hello',
|
|
170
|
+
injected_field: 'malicious',
|
|
171
|
+
});
|
|
172
|
+
expect(result.isError).toBeUndefined();
|
|
173
|
+
const args = JSON.parse(result.content[0].text);
|
|
174
|
+
expect(args.title).toBe('hello');
|
|
175
|
+
expect(args.injected_field).toBeUndefined();
|
|
176
|
+
});
|
|
177
|
+
it('should accumulate ALL validation errors at once', async () => {
|
|
178
|
+
const builder = new GroupedToolBuilder('test')
|
|
179
|
+
.action({
|
|
180
|
+
name: 'create',
|
|
181
|
+
schema: z.object({
|
|
182
|
+
title: z.string(),
|
|
183
|
+
color: z.string(),
|
|
184
|
+
count: z.number(),
|
|
185
|
+
}),
|
|
186
|
+
handler: dummyHandler,
|
|
187
|
+
});
|
|
188
|
+
builder.buildToolDefinition();
|
|
189
|
+
const result = await builder.execute(undefined, {
|
|
190
|
+
action: 'create',
|
|
191
|
+
// Missing: title, color, count
|
|
192
|
+
});
|
|
193
|
+
expect(result.isError).toBe(true);
|
|
194
|
+
const errorText = result.content[0].text;
|
|
195
|
+
// All 3 errors should be present in a single response
|
|
196
|
+
expect(errorText).toContain('title');
|
|
197
|
+
expect(errorText).toContain('color');
|
|
198
|
+
expect(errorText).toContain('count');
|
|
199
|
+
});
|
|
200
|
+
it('should error on unknown action with available list', async () => {
|
|
201
|
+
const builder = new GroupedToolBuilder('test')
|
|
202
|
+
.action({ name: 'list', handler: dummyHandler })
|
|
203
|
+
.action({ name: 'create', handler: dummyHandler });
|
|
204
|
+
builder.buildToolDefinition();
|
|
205
|
+
const result = await builder.execute(undefined, {
|
|
206
|
+
action: 'delete', // doesn't exist
|
|
207
|
+
});
|
|
208
|
+
expect(result.isError).toBe(true);
|
|
209
|
+
expect(result.content[0].text).toContain('Unknown action');
|
|
210
|
+
expect(result.content[0].text).toContain('delete');
|
|
211
|
+
expect(result.content[0].text).toContain('list, create');
|
|
212
|
+
});
|
|
213
|
+
it('should error when discriminator is missing', async () => {
|
|
214
|
+
const builder = new GroupedToolBuilder('test')
|
|
215
|
+
.action({ name: 'list', handler: dummyHandler });
|
|
216
|
+
builder.buildToolDefinition();
|
|
217
|
+
const result = await builder.execute(undefined, {});
|
|
218
|
+
expect(result.isError).toBe(true);
|
|
219
|
+
expect(result.content[0].text).toContain('action is required');
|
|
220
|
+
});
|
|
221
|
+
it('should merge commonSchema and action schema', async () => {
|
|
222
|
+
const builder = new GroupedToolBuilder('test')
|
|
223
|
+
.commonSchema(z.object({
|
|
224
|
+
company_slug: z.string(),
|
|
225
|
+
}))
|
|
226
|
+
.action({
|
|
227
|
+
name: 'create',
|
|
228
|
+
schema: z.object({ title: z.string() }),
|
|
229
|
+
handler: echoHandler,
|
|
230
|
+
});
|
|
231
|
+
builder.buildToolDefinition();
|
|
232
|
+
// Missing company_slug → validation error
|
|
233
|
+
const result1 = await builder.execute(undefined, {
|
|
234
|
+
action: 'create',
|
|
235
|
+
title: 'hello',
|
|
236
|
+
});
|
|
237
|
+
expect(result1.isError).toBe(true);
|
|
238
|
+
expect(result1.content[0].text).toContain('company_slug');
|
|
239
|
+
// Valid
|
|
240
|
+
const result2 = await builder.execute(undefined, {
|
|
241
|
+
action: 'create',
|
|
242
|
+
company_slug: 'acme',
|
|
243
|
+
title: 'hello',
|
|
244
|
+
});
|
|
245
|
+
expect(result2.isError).toBeUndefined();
|
|
246
|
+
});
|
|
247
|
+
it('should freeze builder after buildToolDefinition()', () => {
|
|
248
|
+
const builder = new GroupedToolBuilder('test')
|
|
249
|
+
.action({ name: 'list', handler: dummyHandler });
|
|
250
|
+
builder.buildToolDefinition();
|
|
251
|
+
expect(() => {
|
|
252
|
+
builder.action({ name: 'create', handler: dummyHandler });
|
|
253
|
+
}).toThrow('frozen');
|
|
254
|
+
});
|
|
255
|
+
it('should cache buildToolDefinition result', () => {
|
|
256
|
+
const builder = new GroupedToolBuilder('test')
|
|
257
|
+
.action({ name: 'list', handler: dummyHandler });
|
|
258
|
+
const tool1 = builder.buildToolDefinition();
|
|
259
|
+
const tool2 = builder.buildToolDefinition();
|
|
260
|
+
expect(tool1).toBe(tool2); // Same reference
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
// ============================================================================
|
|
264
|
+
// Annotations
|
|
265
|
+
// ============================================================================
|
|
266
|
+
describe('GroupedToolBuilder — Annotations', () => {
|
|
267
|
+
it('should aggregate readOnlyHint = true when ALL actions are readOnly', () => {
|
|
268
|
+
const builder = new GroupedToolBuilder('query')
|
|
269
|
+
.action({ name: 'list', readOnly: true, handler: dummyHandler })
|
|
270
|
+
.action({ name: 'get', readOnly: true, handler: dummyHandler });
|
|
271
|
+
const tool = builder.buildToolDefinition();
|
|
272
|
+
expect(tool.annotations.readOnlyHint).toBe(true);
|
|
273
|
+
expect(tool.annotations.destructiveHint).toBe(false);
|
|
274
|
+
});
|
|
275
|
+
it('should aggregate destructiveHint = true when ANY action is destructive', () => {
|
|
276
|
+
const builder = new GroupedToolBuilder('crud')
|
|
277
|
+
.action({ name: 'list', readOnly: true, handler: dummyHandler })
|
|
278
|
+
.action({ name: 'delete', destructive: true, handler: dummyHandler });
|
|
279
|
+
const tool = builder.buildToolDefinition();
|
|
280
|
+
expect(tool.annotations.destructiveHint).toBe(true);
|
|
281
|
+
expect(tool.annotations.readOnlyHint).toBe(false);
|
|
282
|
+
});
|
|
283
|
+
it('should add ⚠️ DESTRUCTIVE in workflow description for destructive actions', () => {
|
|
284
|
+
const builder = new GroupedToolBuilder('crud')
|
|
285
|
+
.description('CRUD')
|
|
286
|
+
.action({ name: 'delete', description: 'Delete permanently', destructive: true, handler: dummyHandler });
|
|
287
|
+
const tool = builder.buildToolDefinition();
|
|
288
|
+
expect(tool.description).toContain('⚠️ DESTRUCTIVE');
|
|
289
|
+
});
|
|
290
|
+
it('should aggregate idempotentHint = true when ALL actions are idempotent', () => {
|
|
291
|
+
const builder = new GroupedToolBuilder('api')
|
|
292
|
+
.action({ name: 'get', idempotent: true, handler: dummyHandler })
|
|
293
|
+
.action({ name: 'put', idempotent: true, handler: dummyHandler });
|
|
294
|
+
const tool = builder.buildToolDefinition();
|
|
295
|
+
expect(tool.annotations.idempotentHint).toBe(true);
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
// ============================================================================
|
|
299
|
+
// Middleware
|
|
300
|
+
// ============================================================================
|
|
301
|
+
describe('GroupedToolBuilder — Middleware', () => {
|
|
302
|
+
it('should run middleware before handler', async () => {
|
|
303
|
+
const log = [];
|
|
304
|
+
const builder = new GroupedToolBuilder('test')
|
|
305
|
+
.use(async (_ctx, _args, next) => {
|
|
306
|
+
log.push('middleware');
|
|
307
|
+
return next();
|
|
308
|
+
})
|
|
309
|
+
.action({
|
|
310
|
+
name: 'list',
|
|
311
|
+
handler: async () => {
|
|
312
|
+
log.push('handler');
|
|
313
|
+
return success('ok');
|
|
314
|
+
},
|
|
315
|
+
});
|
|
316
|
+
builder.buildToolDefinition();
|
|
317
|
+
await builder.execute(undefined, { action: 'list' });
|
|
318
|
+
expect(log).toEqual(['middleware', 'handler']);
|
|
319
|
+
});
|
|
320
|
+
it('should support multiple middlewares in order', async () => {
|
|
321
|
+
const log = [];
|
|
322
|
+
const builder = new GroupedToolBuilder('test')
|
|
323
|
+
.use(async (_ctx, _args, next) => {
|
|
324
|
+
log.push('mw1');
|
|
325
|
+
return next();
|
|
326
|
+
})
|
|
327
|
+
.use(async (_ctx, _args, next) => {
|
|
328
|
+
log.push('mw2');
|
|
329
|
+
return next();
|
|
330
|
+
})
|
|
331
|
+
.action({
|
|
332
|
+
name: 'list',
|
|
333
|
+
handler: async () => {
|
|
334
|
+
log.push('handler');
|
|
335
|
+
return success('ok');
|
|
336
|
+
},
|
|
337
|
+
});
|
|
338
|
+
builder.buildToolDefinition();
|
|
339
|
+
await builder.execute(undefined, { action: 'list' });
|
|
340
|
+
expect(log).toEqual(['mw1', 'mw2', 'handler']);
|
|
341
|
+
});
|
|
342
|
+
it('should allow middleware to short-circuit (skip handler)', async () => {
|
|
343
|
+
const builder = new GroupedToolBuilder('test')
|
|
344
|
+
.use(async (_ctx, _args, _next) => {
|
|
345
|
+
return error('unauthorized');
|
|
346
|
+
})
|
|
347
|
+
.action({
|
|
348
|
+
name: 'list',
|
|
349
|
+
handler: async () => success('should not reach'),
|
|
350
|
+
});
|
|
351
|
+
builder.buildToolDefinition();
|
|
352
|
+
const result = await builder.execute(undefined, { action: 'list' });
|
|
353
|
+
expect(result.isError).toBe(true);
|
|
354
|
+
expect(result.content[0].text).toBe('unauthorized');
|
|
355
|
+
});
|
|
356
|
+
it('should pass validated args to middleware (not raw args)', async () => {
|
|
357
|
+
const builder = new GroupedToolBuilder('test')
|
|
358
|
+
.use(async (_ctx, args, next) => {
|
|
359
|
+
// injected_field should be stripped by Zod
|
|
360
|
+
expect(args['injected_field']).toBeUndefined();
|
|
361
|
+
expect(args['title']).toBe('hello');
|
|
362
|
+
return next();
|
|
363
|
+
})
|
|
364
|
+
.action({
|
|
365
|
+
name: 'create',
|
|
366
|
+
schema: z.object({ title: z.string() }),
|
|
367
|
+
handler: dummyHandler,
|
|
368
|
+
});
|
|
369
|
+
builder.buildToolDefinition();
|
|
370
|
+
await builder.execute(undefined, {
|
|
371
|
+
action: 'create',
|
|
372
|
+
title: 'hello',
|
|
373
|
+
injected_field: 'bad',
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
it('should catch handler errors and return error response', async () => {
|
|
377
|
+
const builder = new GroupedToolBuilder('test')
|
|
378
|
+
.action({
|
|
379
|
+
name: 'crash',
|
|
380
|
+
handler: async () => { throw new Error('boom'); },
|
|
381
|
+
});
|
|
382
|
+
builder.buildToolDefinition();
|
|
383
|
+
const result = await builder.execute(undefined, { action: 'crash' });
|
|
384
|
+
expect(result.isError).toBe(true);
|
|
385
|
+
expect(result.content[0].text).toBe('[test/crash] boom');
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
// ============================================================================
|
|
389
|
+
// Per-Field Annotations
|
|
390
|
+
// ============================================================================
|
|
391
|
+
describe('GroupedToolBuilder — Per-Field Annotations', () => {
|
|
392
|
+
it('should annotate common required fields as "(always required)"', () => {
|
|
393
|
+
const builder = new GroupedToolBuilder('test')
|
|
394
|
+
.commonSchema(z.object({
|
|
395
|
+
company_slug: z.string().describe('Workspace ID'),
|
|
396
|
+
}))
|
|
397
|
+
.action({ name: 'list', handler: dummyHandler })
|
|
398
|
+
.action({ name: 'create', handler: dummyHandler });
|
|
399
|
+
const tool = builder.buildToolDefinition();
|
|
400
|
+
const companyField = tool.inputSchema.properties.company_slug;
|
|
401
|
+
expect(companyField.description).toContain('Workspace ID');
|
|
402
|
+
expect(companyField.description).toContain('(always required)');
|
|
403
|
+
});
|
|
404
|
+
it('should annotate action-specific fields with "Required for:"', () => {
|
|
405
|
+
const builder = new GroupedToolBuilder('test')
|
|
406
|
+
.action({
|
|
407
|
+
name: 'list',
|
|
408
|
+
handler: dummyHandler,
|
|
409
|
+
})
|
|
410
|
+
.action({
|
|
411
|
+
name: 'create',
|
|
412
|
+
schema: z.object({ title: z.string() }),
|
|
413
|
+
handler: dummyHandler,
|
|
414
|
+
});
|
|
415
|
+
const tool = builder.buildToolDefinition();
|
|
416
|
+
const titleField = tool.inputSchema.properties.title;
|
|
417
|
+
expect(titleField.description).toContain('Required for: create');
|
|
418
|
+
});
|
|
419
|
+
it('should annotate fields appearing in multiple actions with "For:"', () => {
|
|
420
|
+
const builder = new GroupedToolBuilder('test')
|
|
421
|
+
.action({
|
|
422
|
+
name: 'list',
|
|
423
|
+
schema: z.object({ filter: z.string().optional() }),
|
|
424
|
+
handler: dummyHandler,
|
|
425
|
+
})
|
|
426
|
+
.action({
|
|
427
|
+
name: 'search',
|
|
428
|
+
schema: z.object({ filter: z.string().optional() }),
|
|
429
|
+
handler: dummyHandler,
|
|
430
|
+
});
|
|
431
|
+
const tool = builder.buildToolDefinition();
|
|
432
|
+
const filterField = tool.inputSchema.properties.filter;
|
|
433
|
+
expect(filterField.description).toContain('For: list, search');
|
|
434
|
+
});
|
|
435
|
+
it('should annotate field required in SOME actions and optional in others', () => {
|
|
436
|
+
// "query" is required for "search" but optional for "export"
|
|
437
|
+
// This should produce "Required for: search. For: export"
|
|
438
|
+
const builder = new GroupedToolBuilder('analytics')
|
|
439
|
+
.action({
|
|
440
|
+
name: 'search',
|
|
441
|
+
schema: z.object({ query: z.string() }), // required
|
|
442
|
+
handler: dummyHandler,
|
|
443
|
+
})
|
|
444
|
+
.action({
|
|
445
|
+
name: 'export',
|
|
446
|
+
schema: z.object({ query: z.string().optional() }), // optional
|
|
447
|
+
handler: dummyHandler,
|
|
448
|
+
});
|
|
449
|
+
const tool = builder.buildToolDefinition();
|
|
450
|
+
const queryField = tool.inputSchema.properties.query;
|
|
451
|
+
expect(queryField.description).toContain('Required for: search');
|
|
452
|
+
expect(queryField.description).toContain('For: export');
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
// ============================================================================
|
|
456
|
+
// Scenario: DevOps CI/CD Pipeline Tool
|
|
457
|
+
// Tests: auto-build on execute(), commonSchema-only validation (no action schema)
|
|
458
|
+
// ============================================================================
|
|
459
|
+
describe('Scenario — DevOps CI/CD Pipeline Tool', () => {
|
|
460
|
+
it('should auto-build when execute() is called without buildToolDefinition()', async () => {
|
|
461
|
+
// A CI/CD tool where the user calls execute() directly — the builder
|
|
462
|
+
// should lazily build before routing
|
|
463
|
+
const builder = new GroupedToolBuilder('ci_pipeline')
|
|
464
|
+
.description('CI/CD pipeline operations')
|
|
465
|
+
.commonSchema(z.object({
|
|
466
|
+
repository: z.string(),
|
|
467
|
+
}))
|
|
468
|
+
.action({
|
|
469
|
+
name: 'trigger',
|
|
470
|
+
handler: async (ctx, args) => success(`triggered ${args.repository} by ${ctx.user}`),
|
|
471
|
+
})
|
|
472
|
+
.action({
|
|
473
|
+
name: 'status',
|
|
474
|
+
readOnly: true,
|
|
475
|
+
handler: async (_ctx, args) => success(`status of ${args.repository}: passing`),
|
|
476
|
+
});
|
|
477
|
+
// execute() without calling buildToolDefinition() first
|
|
478
|
+
const result = await builder.execute({ user: 'ci-bot' }, { action: 'trigger', repository: 'org/infra' });
|
|
479
|
+
expect(result.isError).toBeUndefined();
|
|
480
|
+
expect(result.content[0].text).toContain('triggered org/infra by ci-bot');
|
|
481
|
+
});
|
|
482
|
+
it('should validate common schema even when action has no schema', async () => {
|
|
483
|
+
// The tool requires a "repository" field via commonSchema,
|
|
484
|
+
// but the "status" action defines no per-action schema
|
|
485
|
+
const builder = new GroupedToolBuilder('ci_pipeline')
|
|
486
|
+
.commonSchema(z.object({
|
|
487
|
+
repository: z.string(),
|
|
488
|
+
}))
|
|
489
|
+
.action({
|
|
490
|
+
name: 'status',
|
|
491
|
+
readOnly: true,
|
|
492
|
+
handler: async (_ctx, args) => success(`status of ${args.repository}`),
|
|
493
|
+
});
|
|
494
|
+
builder.buildToolDefinition();
|
|
495
|
+
// Missing repository → validation error from commonSchema only
|
|
496
|
+
const result = await builder.execute(undefined, { action: 'status' });
|
|
497
|
+
expect(result.isError).toBe(true);
|
|
498
|
+
expect(result.content[0].text).toContain('repository');
|
|
499
|
+
});
|
|
500
|
+
});
|
|
501
|
+
// ============================================================================
|
|
502
|
+
// Scenario: Database Admin Tool
|
|
503
|
+
// Tests: explicit annotation override of aggregated hints
|
|
504
|
+
// ============================================================================
|
|
505
|
+
describe('Scenario — Database Admin Tool', () => {
|
|
506
|
+
it('should let explicit annotations override per-action aggregated hints', () => {
|
|
507
|
+
// Even though all actions are readOnly, the tool author explicitly
|
|
508
|
+
// sets readOnlyHint=false (e.g., because the tool opens connections)
|
|
509
|
+
// Similarly, overrides destructiveHint and idempotentHint
|
|
510
|
+
const builder = new GroupedToolBuilder('db_admin')
|
|
511
|
+
.description('Database inspection')
|
|
512
|
+
.annotations({
|
|
513
|
+
readOnlyHint: false,
|
|
514
|
+
destructiveHint: true,
|
|
515
|
+
idempotentHint: true,
|
|
516
|
+
})
|
|
517
|
+
.action({
|
|
518
|
+
name: 'list_tables',
|
|
519
|
+
readOnly: true,
|
|
520
|
+
idempotent: true,
|
|
521
|
+
handler: dummyHandler,
|
|
522
|
+
})
|
|
523
|
+
.action({
|
|
524
|
+
name: 'describe_table',
|
|
525
|
+
readOnly: true,
|
|
526
|
+
idempotent: true,
|
|
527
|
+
handler: dummyHandler,
|
|
528
|
+
});
|
|
529
|
+
const tool = builder.buildToolDefinition();
|
|
530
|
+
const annotations = tool.annotations;
|
|
531
|
+
// Explicit overrides take precedence over aggregation
|
|
532
|
+
expect(annotations.readOnlyHint).toBe(false);
|
|
533
|
+
expect(annotations.destructiveHint).toBe(true);
|
|
534
|
+
expect(annotations.idempotentHint).toBe(true);
|
|
535
|
+
});
|
|
536
|
+
});
|
|
537
|
+
// ============================================================================
|
|
538
|
+
// Scenario: IoT Sensor Controller
|
|
539
|
+
// Tests: action-schema-only validation (no commonSchema), non-Error throw
|
|
540
|
+
// ============================================================================
|
|
541
|
+
describe('Scenario — IoT Sensor Controller', () => {
|
|
542
|
+
it('should validate per-action schema when no commonSchema exists', async () => {
|
|
543
|
+
// Each action has its own schema, no shared fields
|
|
544
|
+
const builder = new GroupedToolBuilder('sensor_ctl')
|
|
545
|
+
.description('IoT sensor management')
|
|
546
|
+
.action({
|
|
547
|
+
name: 'read',
|
|
548
|
+
readOnly: true,
|
|
549
|
+
schema: z.object({
|
|
550
|
+
sensor_id: z.string(),
|
|
551
|
+
unit: z.enum(['celsius', 'fahrenheit']),
|
|
552
|
+
}),
|
|
553
|
+
handler: async (_ctx, args) => success(`${args.sensor_id}: 22.5 ${args.unit}`),
|
|
554
|
+
})
|
|
555
|
+
.action({
|
|
556
|
+
name: 'calibrate',
|
|
557
|
+
schema: z.object({
|
|
558
|
+
sensor_id: z.string(),
|
|
559
|
+
offset: z.number(),
|
|
560
|
+
}),
|
|
561
|
+
handler: dummyHandler,
|
|
562
|
+
});
|
|
563
|
+
builder.buildToolDefinition();
|
|
564
|
+
// Valid request — action-only schema validates correctly
|
|
565
|
+
const result = await builder.execute(undefined, {
|
|
566
|
+
action: 'read',
|
|
567
|
+
sensor_id: 'temp-001',
|
|
568
|
+
unit: 'celsius',
|
|
569
|
+
});
|
|
570
|
+
expect(result.isError).toBeUndefined();
|
|
571
|
+
expect(result.content[0].text).toContain('temp-001');
|
|
572
|
+
// Invalid enum value
|
|
573
|
+
const result2 = await builder.execute(undefined, {
|
|
574
|
+
action: 'read',
|
|
575
|
+
sensor_id: 'temp-001',
|
|
576
|
+
unit: 'kelvin', // not in enum
|
|
577
|
+
});
|
|
578
|
+
expect(result2.isError).toBe(true);
|
|
579
|
+
expect(result2.content[0].text).toContain('unit');
|
|
580
|
+
});
|
|
581
|
+
it('should catch non-Error throw values gracefully', async () => {
|
|
582
|
+
// Handler throws a raw string instead of new Error()
|
|
583
|
+
const builder = new GroupedToolBuilder('sensor_ctl')
|
|
584
|
+
.action({
|
|
585
|
+
name: 'reboot',
|
|
586
|
+
handler: async () => { throw 'DEVICE_OFFLINE'; },
|
|
587
|
+
});
|
|
588
|
+
builder.buildToolDefinition();
|
|
589
|
+
const result = await builder.execute(undefined, { action: 'reboot' });
|
|
590
|
+
expect(result.isError).toBe(true);
|
|
591
|
+
expect(result.content[0].text).toBe('[sensor_ctl/reboot] DEVICE_OFFLINE');
|
|
592
|
+
});
|
|
593
|
+
});
|
|
594
|
+
// ============================================================================
|
|
595
|
+
// Scenario: File System Tool (Grouped Mode)
|
|
596
|
+
// Tests: grouped mode with destructive actions, workflow description variants
|
|
597
|
+
// ============================================================================
|
|
598
|
+
describe('Scenario — File System Tool (Grouped)', () => {
|
|
599
|
+
it('should generate correct workflow for grouped actions with mixed annotations', () => {
|
|
600
|
+
const builder = new GroupedToolBuilder('fs')
|
|
601
|
+
.description('File system operations')
|
|
602
|
+
.group('files', 'File operations', g => g
|
|
603
|
+
.action({
|
|
604
|
+
name: 'read',
|
|
605
|
+
description: 'Read file contents',
|
|
606
|
+
readOnly: true,
|
|
607
|
+
schema: z.object({ path: z.string() }),
|
|
608
|
+
handler: dummyHandler,
|
|
609
|
+
})
|
|
610
|
+
.action({
|
|
611
|
+
name: 'write',
|
|
612
|
+
description: 'Write to a file',
|
|
613
|
+
schema: z.object({
|
|
614
|
+
path: z.string(),
|
|
615
|
+
content: z.string(),
|
|
616
|
+
}),
|
|
617
|
+
handler: dummyHandler,
|
|
618
|
+
})
|
|
619
|
+
.action({
|
|
620
|
+
name: 'delete',
|
|
621
|
+
description: 'Permanently delete a file',
|
|
622
|
+
destructive: true,
|
|
623
|
+
schema: z.object({ path: z.string() }),
|
|
624
|
+
handler: dummyHandler,
|
|
625
|
+
}))
|
|
626
|
+
.group('dirs', 'Directory operations', g => g
|
|
627
|
+
.action({
|
|
628
|
+
name: 'list',
|
|
629
|
+
readOnly: true,
|
|
630
|
+
schema: z.object({ path: z.string() }),
|
|
631
|
+
handler: dummyHandler,
|
|
632
|
+
}));
|
|
633
|
+
const tool = builder.buildToolDefinition();
|
|
634
|
+
// Grouped enum format
|
|
635
|
+
const actionProp = tool.inputSchema.properties.action;
|
|
636
|
+
expect(actionProp.enum).toEqual([
|
|
637
|
+
'files.read', 'files.write', 'files.delete', 'dirs.list',
|
|
638
|
+
]);
|
|
639
|
+
// Description includes modules
|
|
640
|
+
expect(tool.description).toContain('Modules:');
|
|
641
|
+
expect(tool.description).toContain('files (read,write,delete)');
|
|
642
|
+
expect(tool.description).toContain('dirs (list)');
|
|
643
|
+
// Workflow includes destructive marker
|
|
644
|
+
expect(tool.description).toContain('⚠️ DESTRUCTIVE');
|
|
645
|
+
// Annotations: not all readOnly (write + delete exist), but delete is destructive
|
|
646
|
+
expect(tool.annotations.readOnlyHint).toBe(false);
|
|
647
|
+
expect(tool.annotations.destructiveHint).toBe(true);
|
|
648
|
+
});
|
|
649
|
+
it('should execute grouped actions correctly with validation', async () => {
|
|
650
|
+
const builder = new GroupedToolBuilder('fs')
|
|
651
|
+
.group('files', 'Files', g => g
|
|
652
|
+
.action({
|
|
653
|
+
name: 'read',
|
|
654
|
+
schema: z.object({ path: z.string() }),
|
|
655
|
+
handler: async (_ctx, args) => success(`content of ${args.path}`),
|
|
656
|
+
}));
|
|
657
|
+
builder.buildToolDefinition();
|
|
658
|
+
const result = await builder.execute(undefined, {
|
|
659
|
+
action: 'files.read',
|
|
660
|
+
path: '/etc/config.yaml',
|
|
661
|
+
});
|
|
662
|
+
expect(result.isError).toBeUndefined();
|
|
663
|
+
expect(result.content[0].text).toBe('content of /etc/config.yaml');
|
|
664
|
+
});
|
|
665
|
+
});
|
|
666
|
+
// ============================================================================
|
|
667
|
+
// Scenario: E-Commerce Tool
|
|
668
|
+
// Tests: workflow "Requires:" without description, introspection methods
|
|
669
|
+
// ============================================================================
|
|
670
|
+
describe('Scenario — E-Commerce Tool', () => {
|
|
671
|
+
it('should show "Requires:" without description prefix in workflow', () => {
|
|
672
|
+
// Action has required fields but no description — workflow should show
|
|
673
|
+
// "Requires: X" directly without a leading description
|
|
674
|
+
const builder = new GroupedToolBuilder('orders')
|
|
675
|
+
.description('Order management')
|
|
676
|
+
.action({
|
|
677
|
+
name: 'list',
|
|
678
|
+
readOnly: true,
|
|
679
|
+
handler: dummyHandler,
|
|
680
|
+
})
|
|
681
|
+
.action({
|
|
682
|
+
name: 'create',
|
|
683
|
+
// No description — just required fields
|
|
684
|
+
schema: z.object({
|
|
685
|
+
product_id: z.string(),
|
|
686
|
+
quantity: z.number(),
|
|
687
|
+
}),
|
|
688
|
+
handler: dummyHandler,
|
|
689
|
+
});
|
|
690
|
+
const tool = builder.buildToolDefinition();
|
|
691
|
+
// Should contain "Requires: product_id, quantity" without a description prefix
|
|
692
|
+
expect(tool.description).toContain("- 'create': Requires: product_id, quantity");
|
|
693
|
+
});
|
|
694
|
+
it('should expose action names via getActionNames()', () => {
|
|
695
|
+
const builder = new GroupedToolBuilder('orders')
|
|
696
|
+
.action({ name: 'list', handler: dummyHandler })
|
|
697
|
+
.action({ name: 'create', handler: dummyHandler })
|
|
698
|
+
.action({ name: 'cancel', handler: dummyHandler });
|
|
699
|
+
builder.buildToolDefinition();
|
|
700
|
+
expect(builder.getName()).toBe('orders');
|
|
701
|
+
expect(builder.getActionNames()).toEqual(['list', 'create', 'cancel']);
|
|
702
|
+
});
|
|
703
|
+
it('should use tool name as description fallback when no description set', () => {
|
|
704
|
+
const builder = new GroupedToolBuilder('orders')
|
|
705
|
+
.action({ name: 'list', handler: dummyHandler });
|
|
706
|
+
const tool = builder.buildToolDefinition();
|
|
707
|
+
// Should use name "orders" as fallback since no .description() was set
|
|
708
|
+
expect(tool.description).toContain('orders');
|
|
709
|
+
expect(tool.description).toContain('Actions: list');
|
|
710
|
+
});
|
|
711
|
+
});
|
|
712
|
+
//# sourceMappingURL=GroupedToolBuilder.test.js.map
|