@mcp-web/core 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 (102) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +253 -0
  3. package/dist/addTool.typetest.d.ts +11 -0
  4. package/dist/addTool.typetest.d.ts.map +1 -0
  5. package/dist/addTool.typetest.js +248 -0
  6. package/dist/create-state-tools.d.ts +77 -0
  7. package/dist/create-state-tools.d.ts.map +1 -0
  8. package/dist/create-state-tools.js +181 -0
  9. package/dist/create-tool.d.ts +90 -0
  10. package/dist/create-tool.d.ts.map +1 -0
  11. package/dist/create-tool.js +82 -0
  12. package/dist/expanded-schema-tools/generate-fixed-shape-tools.d.ts +8 -0
  13. package/dist/expanded-schema-tools/generate-fixed-shape-tools.d.ts.map +1 -0
  14. package/dist/expanded-schema-tools/generate-fixed-shape-tools.js +53 -0
  15. package/dist/expanded-schema-tools/generate-fixed-shape-tools.test.d.ts +2 -0
  16. package/dist/expanded-schema-tools/generate-fixed-shape-tools.test.d.ts.map +1 -0
  17. package/dist/expanded-schema-tools/generate-fixed-shape-tools.test.js +331 -0
  18. package/dist/expanded-schema-tools/index.d.ts +4 -0
  19. package/dist/expanded-schema-tools/index.d.ts.map +1 -0
  20. package/dist/expanded-schema-tools/index.js +2 -0
  21. package/dist/expanded-schema-tools/integration.test.d.ts +2 -0
  22. package/dist/expanded-schema-tools/integration.test.d.ts.map +1 -0
  23. package/dist/expanded-schema-tools/integration.test.js +599 -0
  24. package/dist/expanded-schema-tools/schema-analysis.d.ts +18 -0
  25. package/dist/expanded-schema-tools/schema-analysis.d.ts.map +1 -0
  26. package/dist/expanded-schema-tools/schema-analysis.js +142 -0
  27. package/dist/expanded-schema-tools/schema-analysis.test.d.ts +2 -0
  28. package/dist/expanded-schema-tools/schema-analysis.test.d.ts.map +1 -0
  29. package/dist/expanded-schema-tools/schema-analysis.test.js +314 -0
  30. package/dist/expanded-schema-tools/schema-helpers.d.ts +69 -0
  31. package/dist/expanded-schema-tools/schema-helpers.d.ts.map +1 -0
  32. package/dist/expanded-schema-tools/schema-helpers.js +139 -0
  33. package/dist/expanded-schema-tools/schema-helpers.test.d.ts +2 -0
  34. package/dist/expanded-schema-tools/schema-helpers.test.d.ts.map +1 -0
  35. package/dist/expanded-schema-tools/schema-helpers.test.js +223 -0
  36. package/dist/expanded-schema-tools/tool-generator.d.ts +10 -0
  37. package/dist/expanded-schema-tools/tool-generator.d.ts.map +1 -0
  38. package/dist/expanded-schema-tools/tool-generator.js +430 -0
  39. package/dist/expanded-schema-tools/tool-generator.test.d.ts +2 -0
  40. package/dist/expanded-schema-tools/tool-generator.test.d.ts.map +1 -0
  41. package/dist/expanded-schema-tools/tool-generator.test.js +689 -0
  42. package/dist/expanded-schema-tools/types.d.ts +26 -0
  43. package/dist/expanded-schema-tools/types.d.ts.map +1 -0
  44. package/dist/expanded-schema-tools/types.js +1 -0
  45. package/dist/expanded-schema-tools/utils.d.ts +16 -0
  46. package/dist/expanded-schema-tools/utils.d.ts.map +1 -0
  47. package/dist/expanded-schema-tools/utils.js +35 -0
  48. package/dist/expanded-schema-tools/utils.test.d.ts +2 -0
  49. package/dist/expanded-schema-tools/utils.test.d.ts.map +1 -0
  50. package/dist/expanded-schema-tools/utils.test.js +169 -0
  51. package/dist/group-state.d.ts +60 -0
  52. package/dist/group-state.d.ts.map +1 -0
  53. package/dist/group-state.js +54 -0
  54. package/dist/index.d.ts +14 -0
  55. package/dist/index.d.ts.map +1 -0
  56. package/dist/index.js +13 -0
  57. package/dist/query.d.ts +104 -0
  58. package/dist/query.d.ts.map +1 -0
  59. package/dist/query.js +128 -0
  60. package/dist/schema-helpers.d.ts +69 -0
  61. package/dist/schema-helpers.d.ts.map +1 -0
  62. package/dist/schema-helpers.js +139 -0
  63. package/dist/schemas.d.ts +140 -0
  64. package/dist/schemas.d.ts.map +1 -0
  65. package/dist/schemas.js +70 -0
  66. package/dist/tool-generators/generate-basic-state-tools.d.ts +23 -0
  67. package/dist/tool-generators/generate-basic-state-tools.d.ts.map +1 -0
  68. package/dist/tool-generators/generate-basic-state-tools.js +95 -0
  69. package/dist/tool-generators/generate-fixed-shape-tools.d.ts +8 -0
  70. package/dist/tool-generators/generate-fixed-shape-tools.d.ts.map +1 -0
  71. package/dist/tool-generators/generate-fixed-shape-tools.js +53 -0
  72. package/dist/tool-generators/index.d.ts +6 -0
  73. package/dist/tool-generators/index.d.ts.map +1 -0
  74. package/dist/tool-generators/index.js +3 -0
  75. package/dist/tool-generators/schema-analysis.d.ts +18 -0
  76. package/dist/tool-generators/schema-analysis.d.ts.map +1 -0
  77. package/dist/tool-generators/schema-analysis.js +142 -0
  78. package/dist/tool-generators/schema-helpers.d.ts +87 -0
  79. package/dist/tool-generators/schema-helpers.d.ts.map +1 -0
  80. package/dist/tool-generators/schema-helpers.js +157 -0
  81. package/dist/tool-generators/tool-generator.d.ts +11 -0
  82. package/dist/tool-generators/tool-generator.d.ts.map +1 -0
  83. package/dist/tool-generators/tool-generator.js +437 -0
  84. package/dist/tool-generators/types.d.ts +26 -0
  85. package/dist/tool-generators/types.d.ts.map +1 -0
  86. package/dist/tool-generators/types.js +1 -0
  87. package/dist/tool-generators/utils.d.ts +16 -0
  88. package/dist/tool-generators/utils.d.ts.map +1 -0
  89. package/dist/tool-generators/utils.js +35 -0
  90. package/dist/types.d.ts +17 -0
  91. package/dist/types.d.ts.map +1 -0
  92. package/dist/types.js +1 -0
  93. package/dist/utils.d.ts +31 -0
  94. package/dist/utils.d.ts.map +1 -0
  95. package/dist/utils.js +108 -0
  96. package/dist/web.d.ts +680 -0
  97. package/dist/web.d.ts.map +1 -0
  98. package/dist/web.js +1312 -0
  99. package/dist/zod-to-tools.d.ts +49 -0
  100. package/dist/zod-to-tools.d.ts.map +1 -0
  101. package/dist/zod-to-tools.js +623 -0
  102. package/package.json +58 -0
@@ -0,0 +1,689 @@
1
+ import { test, expect } from 'bun:test';
2
+ import { z } from 'zod';
3
+ import { id, system } from './schema-helpers.js';
4
+ import { generateToolsForSchema } from './tool-generator.js';
5
+ // Mock MCPWeb instance
6
+ const mockMCPWeb = {};
7
+ // ============================================================================
8
+ // Array Tools - Index-Based
9
+ // ============================================================================
10
+ test('generateToolsForSchema() - array generates 4 tools: get, add, set, delete', () => {
11
+ let state = ['a', 'b', 'c'];
12
+ const schema = z.array(z.string());
13
+ const result = generateToolsForSchema({
14
+ name: 'tags',
15
+ description: 'tags',
16
+ get: () => state,
17
+ set: (value) => {
18
+ state = value;
19
+ },
20
+ schema,
21
+ }, mockMCPWeb);
22
+ expect(result.tools).toHaveLength(4);
23
+ expect(result.tools.map((t) => t.name)).toEqual([
24
+ 'get_tags',
25
+ 'add_tags',
26
+ 'set_tags',
27
+ 'delete_tags',
28
+ ]);
29
+ });
30
+ test('generateToolsForSchema() - array get without index returns full array', async () => {
31
+ let state = ['a', 'b', 'c'];
32
+ const schema = z.array(z.string());
33
+ const result = generateToolsForSchema({
34
+ name: 'tags',
35
+ description: 'tags',
36
+ get: () => state,
37
+ set: (value) => {
38
+ state = value;
39
+ },
40
+ schema,
41
+ }, mockMCPWeb);
42
+ const getter = result.tools[0];
43
+ const value = await getter.handler({});
44
+ expect(value).toEqual(['a', 'b', 'c']);
45
+ });
46
+ test('generateToolsForSchema() - array get with index returns item at index', async () => {
47
+ let state = ['a', 'b', 'c'];
48
+ const schema = z.array(z.string());
49
+ const result = generateToolsForSchema({
50
+ name: 'tags',
51
+ description: 'tags',
52
+ get: () => state,
53
+ set: (value) => {
54
+ state = value;
55
+ },
56
+ schema,
57
+ }, mockMCPWeb);
58
+ const getter = result.tools[0];
59
+ expect(await getter.handler({ index: 0 })).toBe('a');
60
+ expect(await getter.handler({ index: 1 })).toBe('b');
61
+ expect(await getter.handler({ index: 2 })).toBe('c');
62
+ });
63
+ test('generateToolsForSchema() - array add without index appends to end', async () => {
64
+ let state = ['a', 'b'];
65
+ const schema = z.array(z.string());
66
+ const result = generateToolsForSchema({
67
+ name: 'tags',
68
+ description: 'tags',
69
+ get: () => state,
70
+ set: (value) => {
71
+ state = value;
72
+ },
73
+ schema,
74
+ }, mockMCPWeb);
75
+ const adder = result.tools[1];
76
+ await adder.handler({ value: 'c' });
77
+ expect(state).toEqual(['a', 'b', 'c']);
78
+ });
79
+ test('generateToolsForSchema() - array add with index inserts at position', async () => {
80
+ let state = ['a', 'c'];
81
+ const schema = z.array(z.string());
82
+ const result = generateToolsForSchema({
83
+ name: 'tags',
84
+ description: 'tags',
85
+ get: () => state,
86
+ set: (value) => {
87
+ state = value;
88
+ },
89
+ schema,
90
+ }, mockMCPWeb);
91
+ const adder = result.tools[1];
92
+ await adder.handler({ value: 'b', index: 1 });
93
+ expect(state).toEqual(['a', 'b', 'c']);
94
+ });
95
+ test('generateToolsForSchema() - array set uses partial update with deep merge', async () => {
96
+ let state = [
97
+ { id: 'a', name: 'Item A', count: 10 },
98
+ { id: 'b', name: 'Item B', count: 20 },
99
+ ];
100
+ const schema = z.array(z.object({
101
+ id: z.string(),
102
+ name: z.string(),
103
+ count: z.number(),
104
+ }));
105
+ const result = generateToolsForSchema({
106
+ name: 'items',
107
+ description: 'items',
108
+ get: () => state,
109
+ set: (value) => {
110
+ state = value;
111
+ },
112
+ schema,
113
+ }, mockMCPWeb);
114
+ const setter = result.tools[2];
115
+ await setter.handler({ index: 0, value: { count: 15 } });
116
+ expect(state[0]).toEqual({ id: 'a', name: 'Item A', count: 15 });
117
+ expect(state[1]).toEqual({ id: 'b', name: 'Item B', count: 20 });
118
+ });
119
+ test('generateToolsForSchema() - array delete by index removes item', async () => {
120
+ let state = ['a', 'b', 'c'];
121
+ const schema = z.array(z.string());
122
+ const result = generateToolsForSchema({
123
+ name: 'tags',
124
+ description: 'tags',
125
+ get: () => state,
126
+ set: (value) => {
127
+ state = value;
128
+ },
129
+ schema,
130
+ }, mockMCPWeb);
131
+ const deleter = result.tools[3];
132
+ await deleter.handler({ index: 1 });
133
+ expect(state).toEqual(['a', 'c']);
134
+ });
135
+ test('generateToolsForSchema() - array delete with all:true clears array', async () => {
136
+ let state = ['a', 'b', 'c'];
137
+ const schema = z.array(z.string());
138
+ const result = generateToolsForSchema({
139
+ name: 'tags',
140
+ description: 'tags',
141
+ get: () => state,
142
+ set: (value) => {
143
+ state = value;
144
+ },
145
+ schema,
146
+ }, mockMCPWeb);
147
+ const deleter = result.tools[3];
148
+ await deleter.handler({ all: true });
149
+ expect(state).toEqual([]);
150
+ });
151
+ test('generateToolsForSchema() - array index out of bounds throws error', async () => {
152
+ let state = ['a', 'b'];
153
+ const schema = z.array(z.string());
154
+ const result = generateToolsForSchema({
155
+ name: 'tags',
156
+ description: 'tags',
157
+ get: () => state,
158
+ set: (value) => {
159
+ state = value;
160
+ },
161
+ schema,
162
+ }, mockMCPWeb);
163
+ const setter = result.tools[2];
164
+ try {
165
+ await setter.handler({ index: 5, value: 'x' });
166
+ expect(true).toBe(false); // Should not reach here
167
+ }
168
+ catch (error) {
169
+ const message = error.message;
170
+ expect(message).toContain('out of bounds');
171
+ expect(message).toContain('5');
172
+ expect(message).toContain('2'); // Array length
173
+ }
174
+ });
175
+ // ============================================================================
176
+ // Array Tools - ID-Based
177
+ // ============================================================================
178
+ test('generateToolsForSchema() - ID-based array generates 4 tools', () => {
179
+ let state = [];
180
+ const schema = z.array(z.object({
181
+ id: id(system(z.string().default(() => crypto.randomUUID()))),
182
+ name: z.string(),
183
+ }));
184
+ const result = generateToolsForSchema({
185
+ name: 'items',
186
+ description: 'items',
187
+ get: () => state,
188
+ set: (value) => {
189
+ state = value;
190
+ },
191
+ schema,
192
+ }, mockMCPWeb);
193
+ expect(result.tools).toHaveLength(4);
194
+ expect(result.tools.map((t) => t.name)).toEqual([
195
+ 'get_items',
196
+ 'add_items',
197
+ 'set_items',
198
+ 'delete_items',
199
+ ]);
200
+ });
201
+ test('generateToolsForSchema() - ID-based get by ID returns matching item', async () => {
202
+ let state = [
203
+ { id: 'abc', name: 'Item A' },
204
+ { id: 'def', name: 'Item B' },
205
+ ];
206
+ const schema = z.array(z.object({
207
+ id: id(z.string()),
208
+ name: z.string(),
209
+ }));
210
+ const result = generateToolsForSchema({
211
+ name: 'items',
212
+ description: 'items',
213
+ get: () => state,
214
+ set: (value) => {
215
+ state = value;
216
+ },
217
+ schema,
218
+ }, mockMCPWeb);
219
+ const getter = result.tools[0];
220
+ const item = await getter.handler({ id: 'abc' });
221
+ expect(item).toEqual({ id: 'abc', name: 'Item A' });
222
+ });
223
+ test('generateToolsForSchema() - ID-based add has no index parameter', () => {
224
+ let state = [];
225
+ const schema = z.array(z.object({
226
+ id: id(system(z.string().default(() => crypto.randomUUID()))),
227
+ name: z.string(),
228
+ }));
229
+ const result = generateToolsForSchema({
230
+ name: 'items',
231
+ description: 'items',
232
+ get: () => state,
233
+ set: (value) => {
234
+ state = value;
235
+ },
236
+ schema,
237
+ }, mockMCPWeb);
238
+ const adder = result.tools[1];
239
+ const inputSchema = adder.inputSchema;
240
+ expect('value' in inputSchema.shape).toBe(true);
241
+ expect('index' in inputSchema.shape).toBe(false);
242
+ });
243
+ test('generateToolsForSchema() - ID-based set finds and updates item', async () => {
244
+ let state = [
245
+ { id: 'abc', name: 'Item A', count: 10 },
246
+ { id: 'def', name: 'Item B', count: 20 },
247
+ ];
248
+ const schema = z.array(z.object({
249
+ id: id(z.string()),
250
+ name: z.string(),
251
+ count: z.number(),
252
+ }));
253
+ const result = generateToolsForSchema({
254
+ name: 'items',
255
+ description: 'items',
256
+ get: () => state,
257
+ set: (value) => {
258
+ state = value;
259
+ },
260
+ schema,
261
+ }, mockMCPWeb);
262
+ const setter = result.tools[2];
263
+ await setter.handler({ id: 'abc', value: { count: 15 } });
264
+ expect(state[0]).toEqual({ id: 'abc', name: 'Item A', count: 15 });
265
+ expect(state[1]).toEqual({ id: 'def', name: 'Item B', count: 20 });
266
+ });
267
+ test('generateToolsForSchema() - ID-based delete removes matching item', async () => {
268
+ let state = [
269
+ { id: 'abc', name: 'Item A' },
270
+ { id: 'def', name: 'Item B' },
271
+ { id: 'ghi', name: 'Item C' },
272
+ ];
273
+ const schema = z.array(z.object({
274
+ id: id(z.string()),
275
+ name: z.string(),
276
+ }));
277
+ const result = generateToolsForSchema({
278
+ name: 'items',
279
+ description: 'items',
280
+ get: () => state,
281
+ set: (value) => {
282
+ state = value;
283
+ },
284
+ schema,
285
+ }, mockMCPWeb);
286
+ const deleter = result.tools[3];
287
+ await deleter.handler({ id: 'def' });
288
+ expect(state).toHaveLength(2);
289
+ expect(state[0].id).toBe('abc');
290
+ expect(state[1].id).toBe('ghi');
291
+ });
292
+ test('generateToolsForSchema() - ID not found throws error', async () => {
293
+ let state = [{ id: 'abc', name: 'Item A' }];
294
+ const schema = z.array(z.object({
295
+ id: id(z.string()),
296
+ name: z.string(),
297
+ }));
298
+ const result = generateToolsForSchema({
299
+ name: 'items',
300
+ description: 'items',
301
+ get: () => state,
302
+ set: (value) => {
303
+ state = value;
304
+ },
305
+ schema,
306
+ }, mockMCPWeb);
307
+ const setter = result.tools[2];
308
+ try {
309
+ await setter.handler({ id: 'nonexistent', value: { name: 'Updated' } });
310
+ expect(true).toBe(false); // Should not reach here
311
+ }
312
+ catch (error) {
313
+ const message = error.message;
314
+ expect(message).toContain('not found');
315
+ expect(message).toContain('nonexistent');
316
+ }
317
+ });
318
+ test('generateToolsForSchema() - ID-based add returns complete object with system fields', async () => {
319
+ let state = [];
320
+ const schema = z.array(z.object({
321
+ id: id(system(z.string().default(() => 'generated-id'))),
322
+ name: z.string(),
323
+ created_at: system(z.number().default(() => 12345)),
324
+ }));
325
+ const result = generateToolsForSchema({
326
+ name: 'items',
327
+ description: 'items',
328
+ get: () => state,
329
+ set: (value) => {
330
+ state = value;
331
+ },
332
+ schema,
333
+ }, mockMCPWeb);
334
+ const adder = result.tools[1];
335
+ const response = (await adder.handler({ value: { name: 'Test' } }));
336
+ expect(response.success).toBe(true);
337
+ expect(response.value.id).toBe('generated-id');
338
+ expect(response.value.name).toBe('Test');
339
+ expect(response.value.created_at).toBe(12345);
340
+ });
341
+ test('generateToolsForSchema() - ID-based system fields excluded from input schemas', () => {
342
+ let state = [];
343
+ const schema = z.array(z.object({
344
+ id: id(system(z.string().default(() => crypto.randomUUID()))),
345
+ name: z.string(),
346
+ created_at: system(z.number().default(() => Date.now())),
347
+ }));
348
+ const result = generateToolsForSchema({
349
+ name: 'items',
350
+ description: 'items',
351
+ get: () => state,
352
+ set: (value) => {
353
+ state = value;
354
+ },
355
+ schema,
356
+ }, mockMCPWeb);
357
+ const adder = result.tools[1];
358
+ const addInputSchema = adder.inputSchema;
359
+ const valueSchema = addInputSchema.shape.value;
360
+ expect('id' in valueSchema.shape).toBe(false);
361
+ expect('created_at' in valueSchema.shape).toBe(false);
362
+ expect('name' in valueSchema.shape).toBe(true);
363
+ });
364
+ // ============================================================================
365
+ // Record Tools
366
+ // ============================================================================
367
+ test('generateToolsForSchema() - record generates 3 tools: get, set, delete', () => {
368
+ let state = {};
369
+ const schema = z.record(z.string(), z.object({ name: z.string() }));
370
+ const result = generateToolsForSchema({
371
+ name: 'projects',
372
+ description: 'projects',
373
+ get: () => state,
374
+ set: (value) => {
375
+ state = value;
376
+ },
377
+ schema,
378
+ }, mockMCPWeb);
379
+ expect(result.tools).toHaveLength(3);
380
+ expect(result.tools.map((t) => t.name)).toEqual([
381
+ 'get_projects',
382
+ 'set_projects',
383
+ 'delete_projects',
384
+ ]);
385
+ });
386
+ test('generateToolsForSchema() - record get by key returns value', async () => {
387
+ let state = {
388
+ proj1: { name: 'Project 1' },
389
+ proj2: { name: 'Project 2' },
390
+ };
391
+ const schema = z.record(z.string(), z.object({ name: z.string() }));
392
+ const result = generateToolsForSchema({
393
+ name: 'projects',
394
+ description: 'projects',
395
+ get: () => state,
396
+ set: (value) => {
397
+ state = value;
398
+ },
399
+ schema,
400
+ }, mockMCPWeb);
401
+ const getter = result.tools[0];
402
+ const value = await getter.handler({ key: 'proj1' });
403
+ expect(value).toEqual({ name: 'Project 1' });
404
+ });
405
+ test('generateToolsForSchema() - record get without key returns full record', async () => {
406
+ let state = {
407
+ proj1: { name: 'Project 1' },
408
+ proj2: { name: 'Project 2' },
409
+ };
410
+ const schema = z.record(z.string(), z.object({ name: z.string() }));
411
+ const result = generateToolsForSchema({
412
+ name: 'projects',
413
+ description: 'projects',
414
+ get: () => state,
415
+ set: (value) => {
416
+ state = value;
417
+ },
418
+ schema,
419
+ }, mockMCPWeb);
420
+ const getter = result.tools[0];
421
+ const value = await getter.handler({});
422
+ expect(value).toEqual(state);
423
+ });
424
+ test('generateToolsForSchema() - record set is upsert (adds or updates)', async () => {
425
+ let state = {
426
+ proj1: { name: 'Project 1' },
427
+ };
428
+ const schema = z.record(z.string(), z.object({ name: z.string() }));
429
+ const result = generateToolsForSchema({
430
+ name: 'projects',
431
+ description: 'projects',
432
+ get: () => state,
433
+ set: (value) => {
434
+ state = value;
435
+ },
436
+ schema,
437
+ }, mockMCPWeb);
438
+ const setter = result.tools[1];
439
+ // Add new entry
440
+ await setter.handler({ key: 'proj2', value: { name: 'Project 2' } });
441
+ expect(state.proj2).toEqual({ name: 'Project 2' });
442
+ // Update existing entry
443
+ await setter.handler({ key: 'proj1', value: { name: 'Updated Project 1' } });
444
+ expect(state.proj1).toEqual({ name: 'Updated Project 1' });
445
+ });
446
+ test('generateToolsForSchema() - record set with existing key uses deep merge for objects', async () => {
447
+ let state = {
448
+ proj1: { name: 'Project 1', count: 10 },
449
+ };
450
+ const schema = z.record(z.string(), z.object({ name: z.string(), count: z.number() }));
451
+ const result = generateToolsForSchema({
452
+ name: 'projects',
453
+ description: 'projects',
454
+ get: () => state,
455
+ set: (value) => {
456
+ state = value;
457
+ },
458
+ schema,
459
+ }, mockMCPWeb);
460
+ const setter = result.tools[1];
461
+ await setter.handler({ key: 'proj1', value: { count: 20 } });
462
+ expect(state.proj1).toEqual({ name: 'Project 1', count: 20 });
463
+ });
464
+ test('generateToolsForSchema() - record delete by key removes entry', async () => {
465
+ let state = {
466
+ proj1: { name: 'Project 1' },
467
+ proj2: { name: 'Project 2' },
468
+ };
469
+ const schema = z.record(z.string(), z.object({ name: z.string() }));
470
+ const result = generateToolsForSchema({
471
+ name: 'projects',
472
+ description: 'projects',
473
+ get: () => state,
474
+ set: (value) => {
475
+ state = value;
476
+ },
477
+ schema,
478
+ }, mockMCPWeb);
479
+ const deleter = result.tools[2];
480
+ await deleter.handler({ key: 'proj1' });
481
+ expect('proj1' in state).toBe(false);
482
+ expect('proj2' in state).toBe(true);
483
+ });
484
+ test('generateToolsForSchema() - record delete with all:true clears record', async () => {
485
+ let state = {
486
+ proj1: { name: 'Project 1' },
487
+ proj2: { name: 'Project 2' },
488
+ };
489
+ const schema = z.record(z.string(), z.object({ name: z.string() }));
490
+ const result = generateToolsForSchema({
491
+ name: 'projects',
492
+ description: 'projects',
493
+ get: () => state,
494
+ set: (value) => {
495
+ state = value;
496
+ },
497
+ schema,
498
+ }, mockMCPWeb);
499
+ const deleter = result.tools[2];
500
+ await deleter.handler({ all: true });
501
+ expect(Object.keys(state)).toHaveLength(0);
502
+ });
503
+ // ============================================================================
504
+ // Mixed Object Tools
505
+ // ============================================================================
506
+ test('generateToolsForSchema() - mixed object generates root get/set + collection tools', () => {
507
+ let state = {
508
+ name: 'App',
509
+ version: 1,
510
+ todos: [],
511
+ projects: {},
512
+ };
513
+ const schema = z.object({
514
+ name: z.string(),
515
+ version: z.number(),
516
+ todos: z.array(z.string()),
517
+ projects: z.record(z.string(), z.object({ name: z.string() })),
518
+ });
519
+ const result = generateToolsForSchema({
520
+ name: 'app',
521
+ description: 'app',
522
+ get: () => state,
523
+ set: (value) => {
524
+ state = value;
525
+ },
526
+ schema,
527
+ }, mockMCPWeb);
528
+ // Root get + root set + todos (4 tools) + projects (3 tools) = 9 tools
529
+ expect(result.tools).toHaveLength(9);
530
+ const toolNames = result.tools.map((t) => t.name);
531
+ expect(toolNames).toContain('get_app');
532
+ expect(toolNames).toContain('set_app');
533
+ expect(toolNames).toContain('get_app_todos');
534
+ expect(toolNames).toContain('add_app_todos');
535
+ expect(toolNames).toContain('set_app_todos');
536
+ expect(toolNames).toContain('delete_app_todos');
537
+ expect(toolNames).toContain('get_app_projects');
538
+ expect(toolNames).toContain('set_app_projects');
539
+ expect(toolNames).toContain('delete_app_projects');
540
+ });
541
+ test('generateToolsForSchema() - mixed object root get returns full state', async () => {
542
+ let state = {
543
+ name: 'App',
544
+ todos: ['a', 'b'],
545
+ };
546
+ const schema = z.object({
547
+ name: z.string(),
548
+ todos: z.array(z.string()),
549
+ });
550
+ const result = generateToolsForSchema({
551
+ name: 'app',
552
+ description: 'app',
553
+ get: () => state,
554
+ set: (value) => {
555
+ state = value;
556
+ },
557
+ schema,
558
+ }, mockMCPWeb);
559
+ const getter = result.tools[0];
560
+ const value = await getter.handler({});
561
+ expect(value).toEqual({ name: 'App', todos: ['a', 'b'] });
562
+ });
563
+ test('generateToolsForSchema() - mixed object root get with excludeCollections returns only fixed props', async () => {
564
+ let state = {
565
+ name: 'App',
566
+ version: 1,
567
+ todos: ['a', 'b'],
568
+ };
569
+ const schema = z.object({
570
+ name: z.string(),
571
+ version: z.number(),
572
+ todos: z.array(z.string()),
573
+ });
574
+ const result = generateToolsForSchema({
575
+ name: 'app',
576
+ description: 'app',
577
+ get: () => state,
578
+ set: (value) => {
579
+ state = value;
580
+ },
581
+ schema,
582
+ }, mockMCPWeb);
583
+ const getter = result.tools[0];
584
+ const value = await getter.handler({ excludeCollections: true });
585
+ expect(value).toEqual({ name: 'App', version: 1 });
586
+ });
587
+ test('generateToolsForSchema() - mixed object root set updates only fixed props', async () => {
588
+ let state = {
589
+ name: 'App',
590
+ version: 1,
591
+ todos: ['a', 'b'],
592
+ };
593
+ const schema = z.object({
594
+ name: z.string(),
595
+ version: z.number(),
596
+ todos: z.array(z.string()),
597
+ });
598
+ const result = generateToolsForSchema({
599
+ name: 'app',
600
+ description: 'app',
601
+ get: () => state,
602
+ set: (value) => {
603
+ state = value;
604
+ },
605
+ schema,
606
+ }, mockMCPWeb);
607
+ const setter = result.tools[1];
608
+ await setter.handler({ version: 2 });
609
+ expect(state.version).toBe(2);
610
+ expect(state.name).toBe('App');
611
+ expect(state.todos).toEqual(['a', 'b']); // Unchanged
612
+ });
613
+ test('generateToolsForSchema() - mixed object generates separate tools for each collection', () => {
614
+ let state = {
615
+ name: 'App',
616
+ todos: [],
617
+ projects: {},
618
+ };
619
+ const schema = z.object({
620
+ name: z.string(),
621
+ todos: z.array(z.string()),
622
+ projects: z.record(z.string(), z.object({ name: z.string() })),
623
+ });
624
+ const result = generateToolsForSchema({
625
+ name: 'app',
626
+ description: 'app',
627
+ get: () => state,
628
+ set: (value) => {
629
+ state = value;
630
+ },
631
+ schema,
632
+ }, mockMCPWeb);
633
+ const toolNames = result.tools.map((t) => t.name);
634
+ // Todos array tools
635
+ expect(toolNames).toContain('get_app_todos');
636
+ expect(toolNames).toContain('add_app_todos');
637
+ expect(toolNames).toContain('set_app_todos');
638
+ expect(toolNames).toContain('delete_app_todos');
639
+ // Projects record tools
640
+ expect(toolNames).toContain('get_app_projects');
641
+ expect(toolNames).toContain('set_app_projects');
642
+ expect(toolNames).toContain('delete_app_projects');
643
+ });
644
+ test('generateToolsForSchema() - object with only dynamic props generates no tools (edge case)', () => {
645
+ let state = {
646
+ todos: [],
647
+ projects: {},
648
+ };
649
+ const schema = z.object({
650
+ todos: z.array(z.string()),
651
+ projects: z.record(z.string(), z.object({ name: z.string() })),
652
+ });
653
+ const result = generateToolsForSchema({
654
+ name: 'app',
655
+ description: 'app',
656
+ get: () => state,
657
+ set: (value) => {
658
+ state = value;
659
+ },
660
+ schema,
661
+ }, mockMCPWeb);
662
+ // Object with only dynamic props is type:'dynamic' and subtype:'object'
663
+ // This case is not handled in the tool generator, so no tools are generated
664
+ // (This is an edge case - normally you'd want mixed object behavior)
665
+ expect(result.tools).toHaveLength(0);
666
+ });
667
+ // ============================================================================
668
+ // Warnings
669
+ // ============================================================================
670
+ test('generateToolsForSchema() - warns about optional fields', () => {
671
+ let state = { name: 'Test', email: undefined };
672
+ const schema = z.object({
673
+ name: z.string(),
674
+ email: z.string().optional(),
675
+ });
676
+ const result = generateToolsForSchema({
677
+ name: 'user',
678
+ description: 'user',
679
+ get: () => state,
680
+ set: (value) => {
681
+ state = value;
682
+ },
683
+ schema,
684
+ }, mockMCPWeb);
685
+ expect(result.warnings).toHaveLength(1);
686
+ expect(result.warnings[0]).toContain('optional()');
687
+ expect(result.warnings[0]).toContain('email');
688
+ expect(result.warnings[0]).toContain('nullable()');
689
+ });