@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.
Files changed (211) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +391 -0
  3. package/dist/src/AbstractBase.d.ts +24 -0
  4. package/dist/src/AbstractBase.d.ts.map +1 -0
  5. package/dist/src/AbstractBase.js +63 -0
  6. package/dist/src/AbstractBase.js.map +1 -0
  7. package/dist/src/AbstractLeaf.d.ts +12 -0
  8. package/dist/src/AbstractLeaf.d.ts.map +1 -0
  9. package/dist/src/AbstractLeaf.js +32 -0
  10. package/dist/src/AbstractLeaf.js.map +1 -0
  11. package/dist/src/Annotations.d.ts +15 -0
  12. package/dist/src/Annotations.d.ts.map +1 -0
  13. package/dist/src/Annotations.js +29 -0
  14. package/dist/src/Annotations.js.map +1 -0
  15. package/dist/src/Group.d.ts +32 -0
  16. package/dist/src/Group.d.ts.map +1 -0
  17. package/dist/src/Group.js +131 -0
  18. package/dist/src/Group.js.map +1 -0
  19. package/dist/src/Icon.d.ts +19 -0
  20. package/dist/src/Icon.d.ts.map +1 -0
  21. package/dist/src/Icon.js +33 -0
  22. package/dist/src/Icon.js.map +1 -0
  23. package/dist/src/Prompt.d.ts +11 -0
  24. package/dist/src/Prompt.d.ts.map +1 -0
  25. package/dist/src/Prompt.js +28 -0
  26. package/dist/src/Prompt.js.map +1 -0
  27. package/dist/src/PromptArgument.d.ts +10 -0
  28. package/dist/src/PromptArgument.d.ts.map +1 -0
  29. package/dist/src/PromptArgument.js +20 -0
  30. package/dist/src/PromptArgument.js.map +1 -0
  31. package/dist/src/Resource.d.ts +19 -0
  32. package/dist/src/Resource.d.ts.map +1 -0
  33. package/dist/src/Resource.js +34 -0
  34. package/dist/src/Resource.js.map +1 -0
  35. package/dist/src/Role.d.ts +5 -0
  36. package/dist/src/Role.d.ts.map +1 -0
  37. package/dist/src/Role.js +6 -0
  38. package/dist/src/Role.js.map +1 -0
  39. package/dist/src/Tool.d.ts +16 -0
  40. package/dist/src/Tool.d.ts.map +1 -0
  41. package/dist/src/Tool.js +28 -0
  42. package/dist/src/Tool.js.map +1 -0
  43. package/dist/src/ToolAnnotations.d.ts +23 -0
  44. package/dist/src/ToolAnnotations.d.ts.map +1 -0
  45. package/dist/src/ToolAnnotations.js +44 -0
  46. package/dist/src/ToolAnnotations.js.map +1 -0
  47. package/dist/src/converters/GroupConverter.d.ts +14 -0
  48. package/dist/src/converters/GroupConverter.d.ts.map +1 -0
  49. package/dist/src/converters/GroupConverter.js +13 -0
  50. package/dist/src/converters/GroupConverter.js.map +1 -0
  51. package/dist/src/converters/PromptConverter.d.ts +14 -0
  52. package/dist/src/converters/PromptConverter.d.ts.map +1 -0
  53. package/dist/src/converters/PromptConverter.js +13 -0
  54. package/dist/src/converters/PromptConverter.js.map +1 -0
  55. package/dist/src/converters/ResourceConverter.d.ts +14 -0
  56. package/dist/src/converters/ResourceConverter.d.ts.map +1 -0
  57. package/dist/src/converters/ResourceConverter.js +13 -0
  58. package/dist/src/converters/ResourceConverter.js.map +1 -0
  59. package/dist/src/converters/ToolAnnotationsConverter.d.ts +16 -0
  60. package/dist/src/converters/ToolAnnotationsConverter.d.ts.map +1 -0
  61. package/dist/src/converters/ToolAnnotationsConverter.js +23 -0
  62. package/dist/src/converters/ToolAnnotationsConverter.js.map +1 -0
  63. package/dist/src/converters/ToolConverter.d.ts +14 -0
  64. package/dist/src/converters/ToolConverter.d.ts.map +1 -0
  65. package/dist/src/converters/ToolConverter.js +13 -0
  66. package/dist/src/converters/ToolConverter.js.map +1 -0
  67. package/dist/src/converters/index.d.ts +6 -0
  68. package/dist/src/converters/index.d.ts.map +1 -0
  69. package/dist/src/converters/index.js +6 -0
  70. package/dist/src/converters/index.js.map +1 -0
  71. package/dist/src/framework/GroupedToolBuilder.d.ts +137 -0
  72. package/dist/src/framework/GroupedToolBuilder.d.ts.map +1 -0
  73. package/dist/src/framework/GroupedToolBuilder.js +289 -0
  74. package/dist/src/framework/GroupedToolBuilder.js.map +1 -0
  75. package/dist/src/framework/ResponseHelper.d.ts +43 -0
  76. package/dist/src/framework/ResponseHelper.d.ts.map +1 -0
  77. package/dist/src/framework/ResponseHelper.js +49 -0
  78. package/dist/src/framework/ResponseHelper.js.map +1 -0
  79. package/dist/src/framework/ToolBuilder.d.ts +46 -0
  80. package/dist/src/framework/ToolBuilder.d.ts.map +1 -0
  81. package/dist/src/framework/ToolBuilder.js +2 -0
  82. package/dist/src/framework/ToolBuilder.js.map +1 -0
  83. package/dist/src/framework/ToolRegistry.d.ts +85 -0
  84. package/dist/src/framework/ToolRegistry.d.ts.map +1 -0
  85. package/dist/src/framework/ToolRegistry.js +153 -0
  86. package/dist/src/framework/ToolRegistry.js.map +1 -0
  87. package/dist/src/framework/index.d.ts +9 -0
  88. package/dist/src/framework/index.d.ts.map +1 -0
  89. package/dist/src/framework/index.js +8 -0
  90. package/dist/src/framework/index.js.map +1 -0
  91. package/dist/src/framework/strategies/AnnotationAggregator.d.ts +11 -0
  92. package/dist/src/framework/strategies/AnnotationAggregator.d.ts.map +1 -0
  93. package/dist/src/framework/strategies/AnnotationAggregator.js +25 -0
  94. package/dist/src/framework/strategies/AnnotationAggregator.js.map +1 -0
  95. package/dist/src/framework/strategies/DescriptionGenerator.d.ts +12 -0
  96. package/dist/src/framework/strategies/DescriptionGenerator.d.ts.map +1 -0
  97. package/dist/src/framework/strategies/DescriptionGenerator.js +70 -0
  98. package/dist/src/framework/strategies/DescriptionGenerator.js.map +1 -0
  99. package/dist/src/framework/strategies/MiddlewareCompiler.d.ts +13 -0
  100. package/dist/src/framework/strategies/MiddlewareCompiler.d.ts.map +1 -0
  101. package/dist/src/framework/strategies/MiddlewareCompiler.js +24 -0
  102. package/dist/src/framework/strategies/MiddlewareCompiler.js.map +1 -0
  103. package/dist/src/framework/strategies/SchemaGenerator.d.ts +15 -0
  104. package/dist/src/framework/strategies/SchemaGenerator.d.ts.map +1 -0
  105. package/dist/src/framework/strategies/SchemaGenerator.js +97 -0
  106. package/dist/src/framework/strategies/SchemaGenerator.js.map +1 -0
  107. package/dist/src/framework/strategies/SchemaUtils.d.ts +7 -0
  108. package/dist/src/framework/strategies/SchemaUtils.d.ts.map +1 -0
  109. package/dist/src/framework/strategies/SchemaUtils.js +17 -0
  110. package/dist/src/framework/strategies/SchemaUtils.js.map +1 -0
  111. package/dist/src/framework/strategies/ToonDescriptionGenerator.d.ts +3 -0
  112. package/dist/src/framework/strategies/ToonDescriptionGenerator.d.ts.map +1 -0
  113. package/dist/src/framework/strategies/ToonDescriptionGenerator.js +53 -0
  114. package/dist/src/framework/strategies/ToonDescriptionGenerator.js.map +1 -0
  115. package/dist/src/framework/strategies/Types.d.ts +34 -0
  116. package/dist/src/framework/strategies/Types.d.ts.map +1 -0
  117. package/dist/src/framework/strategies/Types.js +2 -0
  118. package/dist/src/framework/strategies/Types.js.map +1 -0
  119. package/dist/src/framework/strategies/index.d.ts +12 -0
  120. package/dist/src/framework/strategies/index.d.ts.map +1 -0
  121. package/dist/src/framework/strategies/index.js +11 -0
  122. package/dist/src/framework/strategies/index.js.map +1 -0
  123. package/dist/src/index.d.ts +15 -0
  124. package/dist/src/index.d.ts.map +1 -0
  125. package/dist/src/index.js +15 -0
  126. package/dist/src/index.js.map +1 -0
  127. package/dist/tests/AbstractBase.test.d.ts +2 -0
  128. package/dist/tests/AbstractBase.test.d.ts.map +1 -0
  129. package/dist/tests/AbstractBase.test.js +130 -0
  130. package/dist/tests/AbstractBase.test.js.map +1 -0
  131. package/dist/tests/AbstractLeaf.test.d.ts +2 -0
  132. package/dist/tests/AbstractLeaf.test.d.ts.map +1 -0
  133. package/dist/tests/AbstractLeaf.test.js +65 -0
  134. package/dist/tests/AbstractLeaf.test.js.map +1 -0
  135. package/dist/tests/Annotations.test.d.ts +2 -0
  136. package/dist/tests/Annotations.test.d.ts.map +1 -0
  137. package/dist/tests/Annotations.test.js +34 -0
  138. package/dist/tests/Annotations.test.js.map +1 -0
  139. package/dist/tests/BarrelExport.test.d.ts +2 -0
  140. package/dist/tests/BarrelExport.test.d.ts.map +1 -0
  141. package/dist/tests/BarrelExport.test.js +42 -0
  142. package/dist/tests/BarrelExport.test.js.map +1 -0
  143. package/dist/tests/Converters.test.d.ts +2 -0
  144. package/dist/tests/Converters.test.d.ts.map +1 -0
  145. package/dist/tests/Converters.test.js +193 -0
  146. package/dist/tests/Converters.test.js.map +1 -0
  147. package/dist/tests/Group.test.d.ts +2 -0
  148. package/dist/tests/Group.test.d.ts.map +1 -0
  149. package/dist/tests/Group.test.js +257 -0
  150. package/dist/tests/Group.test.js.map +1 -0
  151. package/dist/tests/Icon.test.d.ts +2 -0
  152. package/dist/tests/Icon.test.d.ts.map +1 -0
  153. package/dist/tests/Icon.test.js +44 -0
  154. package/dist/tests/Icon.test.js.map +1 -0
  155. package/dist/tests/Prompt.test.d.ts +2 -0
  156. package/dist/tests/Prompt.test.d.ts.map +1 -0
  157. package/dist/tests/Prompt.test.js +63 -0
  158. package/dist/tests/Prompt.test.js.map +1 -0
  159. package/dist/tests/PromptArgument.test.d.ts +2 -0
  160. package/dist/tests/PromptArgument.test.d.ts.map +1 -0
  161. package/dist/tests/PromptArgument.test.js +37 -0
  162. package/dist/tests/PromptArgument.test.js.map +1 -0
  163. package/dist/tests/Resource.test.d.ts +2 -0
  164. package/dist/tests/Resource.test.d.ts.map +1 -0
  165. package/dist/tests/Resource.test.js +61 -0
  166. package/dist/tests/Resource.test.js.map +1 -0
  167. package/dist/tests/Role.test.d.ts +2 -0
  168. package/dist/tests/Role.test.d.ts.map +1 -0
  169. package/dist/tests/Role.test.js +17 -0
  170. package/dist/tests/Role.test.js.map +1 -0
  171. package/dist/tests/Tool.test.d.ts +2 -0
  172. package/dist/tests/Tool.test.d.ts.map +1 -0
  173. package/dist/tests/Tool.test.js +62 -0
  174. package/dist/tests/Tool.test.js.map +1 -0
  175. package/dist/tests/ToolAnnotations.test.d.ts +2 -0
  176. package/dist/tests/ToolAnnotations.test.d.ts.map +1 -0
  177. package/dist/tests/ToolAnnotations.test.js +55 -0
  178. package/dist/tests/ToolAnnotations.test.js.map +1 -0
  179. package/dist/tests/framework/AdversarialQA.test.d.ts +2 -0
  180. package/dist/tests/framework/AdversarialQA.test.d.ts.map +1 -0
  181. package/dist/tests/framework/AdversarialQA.test.js +906 -0
  182. package/dist/tests/framework/AdversarialQA.test.js.map +1 -0
  183. package/dist/tests/framework/GroupedToolBuilder.test.d.ts +2 -0
  184. package/dist/tests/framework/GroupedToolBuilder.test.d.ts.map +1 -0
  185. package/dist/tests/framework/GroupedToolBuilder.test.js +712 -0
  186. package/dist/tests/framework/GroupedToolBuilder.test.js.map +1 -0
  187. package/dist/tests/framework/LargeScaleScenarios.test.d.ts +2 -0
  188. package/dist/tests/framework/LargeScaleScenarios.test.d.ts.map +1 -0
  189. package/dist/tests/framework/LargeScaleScenarios.test.js +1043 -0
  190. package/dist/tests/framework/LargeScaleScenarios.test.js.map +1 -0
  191. package/dist/tests/framework/McpServerAdapter.test.d.ts +2 -0
  192. package/dist/tests/framework/McpServerAdapter.test.d.ts.map +1 -0
  193. package/dist/tests/framework/McpServerAdapter.test.js +380 -0
  194. package/dist/tests/framework/McpServerAdapter.test.js.map +1 -0
  195. package/dist/tests/framework/ResponseHelper.test.d.ts +2 -0
  196. package/dist/tests/framework/ResponseHelper.test.d.ts.map +1 -0
  197. package/dist/tests/framework/ResponseHelper.test.js +202 -0
  198. package/dist/tests/framework/ResponseHelper.test.js.map +1 -0
  199. package/dist/tests/framework/SecurityDeep.test.d.ts +2 -0
  200. package/dist/tests/framework/SecurityDeep.test.d.ts.map +1 -0
  201. package/dist/tests/framework/SecurityDeep.test.js +825 -0
  202. package/dist/tests/framework/SecurityDeep.test.js.map +1 -0
  203. package/dist/tests/framework/ToolRegistry.test.d.ts +2 -0
  204. package/dist/tests/framework/ToolRegistry.test.d.ts.map +1 -0
  205. package/dist/tests/framework/ToolRegistry.test.js +152 -0
  206. package/dist/tests/framework/ToolRegistry.test.js.map +1 -0
  207. package/dist/tests/framework/ToonDescription.test.d.ts +2 -0
  208. package/dist/tests/framework/ToonDescription.test.d.ts.map +1 -0
  209. package/dist/tests/framework/ToonDescription.test.js +287 -0
  210. package/dist/tests/framework/ToonDescription.test.js.map +1 -0
  211. 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