@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,223 @@
1
+ import { expect, test } from 'bun:test';
2
+ import { z } from 'zod';
3
+ import { deriveAddInputSchema, deriveSetInputSchema, hasDefault, id, isKeyField, isSystemField, system, unwrapDefault, unwrapSchema, validateSystemFields, } from './schema-helpers.js';
4
+ // ============================================================================
5
+ // Schema Markers
6
+ // ============================================================================
7
+ test('id() - marks field as unique identifier', () => {
8
+ const schema = id(z.string());
9
+ expect(isKeyField(schema)).toBe(true);
10
+ });
11
+ test('system() - marks field as system-generated', () => {
12
+ const schema = system(z.string().default('auto'));
13
+ expect(isSystemField(schema)).toBe(true);
14
+ });
15
+ test('isKeyField() - detects id() marker', () => {
16
+ const markedSchema = id(z.string());
17
+ const normalSchema = z.string();
18
+ expect(isKeyField(markedSchema)).toBe(true);
19
+ expect(isKeyField(normalSchema)).toBe(false);
20
+ });
21
+ test('isSystemField() - detects system() marker', () => {
22
+ const markedSchema = system(z.string().default('auto'));
23
+ const normalSchema = z.string();
24
+ expect(isSystemField(markedSchema)).toBe(true);
25
+ expect(isSystemField(normalSchema)).toBe(false);
26
+ });
27
+ test('id() and system() can be combined', () => {
28
+ const schema = id(system(z.string().default(() => crypto.randomUUID())));
29
+ expect(isKeyField(schema)).toBe(true);
30
+ expect(isSystemField(schema)).toBe(true);
31
+ });
32
+ // ============================================================================
33
+ // Schema Unwrapping
34
+ // ============================================================================
35
+ test('unwrapSchema() - unwraps ZodDefault', () => {
36
+ const schema = z.string().default('hello');
37
+ const unwrapped = unwrapSchema(schema);
38
+ expect(unwrapped).toBeInstanceOf(z.ZodString);
39
+ });
40
+ test('unwrapSchema() - unwraps ZodOptional', () => {
41
+ const schema = z.string().optional();
42
+ const unwrapped = unwrapSchema(schema);
43
+ expect(unwrapped).toBeInstanceOf(z.ZodString);
44
+ });
45
+ test('unwrapSchema() - unwraps ZodNullable', () => {
46
+ const schema = z.string().nullable();
47
+ const unwrapped = unwrapSchema(schema);
48
+ expect(unwrapped).toBeInstanceOf(z.ZodString);
49
+ });
50
+ test('unwrapSchema() - handles nested wrappers', () => {
51
+ const schema = z.string().optional().default('hello').nullable();
52
+ const unwrapped = unwrapSchema(schema);
53
+ expect(unwrapped).toBeInstanceOf(z.ZodString);
54
+ });
55
+ test('unwrapSchema() - returns same schema if not wrapped', () => {
56
+ const schema = z.string();
57
+ const unwrapped = unwrapSchema(schema);
58
+ expect(unwrapped).toBe(schema);
59
+ });
60
+ test('unwrapDefault() - unwraps only ZodDefault', () => {
61
+ const schema = z.string().default('hello');
62
+ const unwrapped = unwrapDefault(schema);
63
+ expect(unwrapped).toBeInstanceOf(z.ZodString);
64
+ });
65
+ test('unwrapDefault() - returns same schema if not ZodDefault', () => {
66
+ const schema = z.string().optional();
67
+ const unwrapped = unwrapDefault(schema);
68
+ expect(unwrapped).toBe(schema);
69
+ });
70
+ // ============================================================================
71
+ // Default Detection
72
+ // ============================================================================
73
+ test('hasDefault() - detects ZodDefault wrapper', () => {
74
+ const withDefault = z.string().default('hello');
75
+ const withoutDefault = z.string();
76
+ expect(hasDefault(withDefault)).toBe(true);
77
+ expect(hasDefault(withoutDefault)).toBe(false);
78
+ });
79
+ test('hasDefault() - works with nullable defaults', () => {
80
+ const schema = z.string().nullable().default(null);
81
+ expect(hasDefault(schema)).toBe(true);
82
+ });
83
+ test('hasDefault() - returns false for optional without default', () => {
84
+ const schema = z.string().optional();
85
+ expect(hasDefault(schema)).toBe(false);
86
+ });
87
+ // ============================================================================
88
+ // Input Schema Derivation - Add
89
+ // ============================================================================
90
+ test('deriveAddInputSchema() - excludes system() fields', () => {
91
+ const schema = z.object({
92
+ id: system(z.string().default(() => crypto.randomUUID())),
93
+ value: z.string(),
94
+ priority: z.number().default(3),
95
+ });
96
+ const addSchema = deriveAddInputSchema(schema);
97
+ const shape = addSchema.shape;
98
+ expect('id' in shape).toBe(false);
99
+ expect('value' in shape).toBe(true);
100
+ expect('priority' in shape).toBe(true);
101
+ });
102
+ test('deriveAddInputSchema() - makes default() fields optional', () => {
103
+ const schema = z.object({
104
+ required: z.string(),
105
+ withDefault: z.number().default(5),
106
+ });
107
+ const addSchema = deriveAddInputSchema(schema);
108
+ // Required field should be required
109
+ const requiredResult = addSchema.safeParse({ required: 'test' });
110
+ expect(requiredResult.success).toBe(true);
111
+ // Field with default should be optional
112
+ const optionalResult = addSchema.safeParse({ required: 'test' });
113
+ expect(optionalResult.success).toBe(true);
114
+ // Can also provide the default field
115
+ const withBothResult = addSchema.safeParse({
116
+ required: 'test',
117
+ withDefault: 10,
118
+ });
119
+ expect(withBothResult.success).toBe(true);
120
+ });
121
+ test('deriveAddInputSchema() - keeps required fields required', () => {
122
+ const schema = z.object({
123
+ name: z.string(),
124
+ age: z.number(),
125
+ });
126
+ const addSchema = deriveAddInputSchema(schema);
127
+ // Should fail without required fields
128
+ const missingName = addSchema.safeParse({ age: 30 });
129
+ expect(missingName.success).toBe(false);
130
+ const missingAge = addSchema.safeParse({ name: 'Alice' });
131
+ expect(missingAge.success).toBe(false);
132
+ // Should pass with both fields
133
+ const complete = addSchema.safeParse({ name: 'Alice', age: 30 });
134
+ expect(complete.success).toBe(true);
135
+ });
136
+ // ============================================================================
137
+ // Input Schema Derivation - Set
138
+ // ============================================================================
139
+ test('deriveSetInputSchema() - excludes system() fields', () => {
140
+ const schema = z.object({
141
+ id: system(z.string().default(() => crypto.randomUUID())),
142
+ updated_at: system(z.number().default(() => Date.now())),
143
+ value: z.string(),
144
+ });
145
+ const setSchema = deriveSetInputSchema(schema);
146
+ const shape = setSchema.shape;
147
+ expect('id' in shape).toBe(false);
148
+ expect('updated_at' in shape).toBe(false);
149
+ expect('value' in shape).toBe(true);
150
+ });
151
+ test('deriveSetInputSchema() - makes all fields optional', () => {
152
+ const schema = z.object({
153
+ name: z.string(),
154
+ age: z.number(),
155
+ email: z.string(),
156
+ });
157
+ const setSchema = deriveSetInputSchema(schema);
158
+ // All fields optional - any combination should work
159
+ expect(setSchema.safeParse({}).success).toBe(true);
160
+ expect(setSchema.safeParse({ name: 'Alice' }).success).toBe(true);
161
+ expect(setSchema.safeParse({ age: 30 }).success).toBe(true);
162
+ expect(setSchema.safeParse({ name: 'Alice', age: 30 }).success).toBe(true);
163
+ expect(setSchema.safeParse({ name: 'Alice', age: 30, email: 'alice@example.com' })
164
+ .success).toBe(true);
165
+ });
166
+ // ============================================================================
167
+ // Validation
168
+ // ============================================================================
169
+ test('validateSystemFields() - passes when system fields have defaults', () => {
170
+ const schema = z.object({
171
+ id: system(z.string().default(() => crypto.randomUUID())),
172
+ created_at: system(z.number().default(() => Date.now())),
173
+ value: z.string(),
174
+ });
175
+ expect(() => validateSystemFields(schema)).not.toThrow();
176
+ });
177
+ test('validateSystemFields() - throws when system field lacks default', () => {
178
+ const schema = z.object({
179
+ id: system(z.string()),
180
+ value: z.string(),
181
+ });
182
+ expect(() => validateSystemFields(schema)).toThrow();
183
+ });
184
+ test('validateSystemFields() - error message includes field name', () => {
185
+ const schema = z.object({
186
+ timestamp: system(z.number()),
187
+ });
188
+ try {
189
+ validateSystemFields(schema);
190
+ expect(true).toBe(false); // Should not reach here
191
+ }
192
+ catch (error) {
193
+ const message = error.message;
194
+ expect(message).toContain('timestamp');
195
+ expect(message).toContain('system()');
196
+ expect(message).toContain('default');
197
+ }
198
+ });
199
+ test('validateSystemFields() - error message suggests fix', () => {
200
+ const schema = z.object({
201
+ created_at: system(z.number()),
202
+ });
203
+ try {
204
+ validateSystemFields(schema);
205
+ expect(true).toBe(false); // Should not reach here
206
+ }
207
+ catch (error) {
208
+ const message = error.message;
209
+ expect(message).toContain('Fix:');
210
+ expect(message).toContain('.default(');
211
+ }
212
+ });
213
+ test('validateSystemFields() - passes with empty object', () => {
214
+ const schema = z.object({});
215
+ expect(() => validateSystemFields(schema)).not.toThrow();
216
+ });
217
+ test('validateSystemFields() - passes with no system fields', () => {
218
+ const schema = z.object({
219
+ name: z.string(),
220
+ age: z.number(),
221
+ });
222
+ expect(() => validateSystemFields(schema)).not.toThrow();
223
+ });
@@ -0,0 +1,10 @@
1
+ import type { MCPWeb } from '../web.js';
2
+ import type { GeneratedTools, ToolGenerationOptions } from './types.js';
3
+ /**
4
+ * Generates MCP tools for a schema based on its shape.
5
+ * - Fixed-shape schemas → get + set with deep merge
6
+ * - Dynamic-shape schemas → get + add + set + delete (arrays) or get + set + delete (records)
7
+ * - Mixed schemas → asymmetric get/set + collection tools
8
+ */
9
+ export declare function generateToolsForSchema(options: ToolGenerationOptions, mcpWeb: MCPWeb): GeneratedTools;
10
+ //# sourceMappingURL=tool-generator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-generator.d.ts","sourceRoot":"","sources":["../../src/expanded-schema-tools/tool-generator.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AASxC,OAAO,KAAK,EAAE,cAAc,EAA+B,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAOrG;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,qBAAqB,EAC9B,MAAM,EAAE,MAAM,GACb,cAAc,CAsDhB"}
@@ -0,0 +1,430 @@
1
+ import { z } from 'zod';
2
+ import { generateFixedShapeTools } from './generate-fixed-shape-tools.js';
3
+ import { analyzeSchemaShape, findIdField } from './schema-analysis.js';
4
+ import { deriveAddInputSchema, deriveSetInputSchema, unwrapSchema, validateSystemFields, } from './schema-helpers.js';
5
+ import { deepMerge } from './utils.js';
6
+ // ============================================================================
7
+ // Main Entry Point
8
+ // ============================================================================
9
+ /**
10
+ * Generates MCP tools for a schema based on its shape.
11
+ * - Fixed-shape schemas → get + set with deep merge
12
+ * - Dynamic-shape schemas → get + add + set + delete (arrays) or get + set + delete (records)
13
+ * - Mixed schemas → asymmetric get/set + collection tools
14
+ */
15
+ export function generateToolsForSchema(options, mcpWeb) {
16
+ const { name, schema } = options;
17
+ const tools = [];
18
+ const warnings = [];
19
+ // Validate schema
20
+ const unwrapped = unwrapSchema(schema);
21
+ // Validate system fields if it's an object
22
+ if (unwrapped instanceof z.ZodObject) {
23
+ validateSystemFields(unwrapped);
24
+ }
25
+ // Analyze schema shape
26
+ const shape = analyzeSchemaShape(schema);
27
+ // Warn about optional fields (once)
28
+ if (shape.hasOptionalFields) {
29
+ warnings.push(`⚠️ Warning: Schema for '${name}' uses optional() on field(s): ${shape.optionalPaths.join(', ')}.\n\n` +
30
+ `Problem: optional() creates ambiguity in partial updates:\n` +
31
+ `- Is the field missing because AI didn't provide it? (keep current)\n` +
32
+ `- Or because AI wants to clear it? (set to undefined)\n\n` +
33
+ `Solution: Use nullable() instead:\n` +
34
+ `- fieldName: z.string().nullable()\n\n` +
35
+ `This makes intent explicit:\n` +
36
+ `- Omit field → keep current value\n` +
37
+ `- Pass null → clear the value`);
38
+ }
39
+ // Generate tools based on schema type
40
+ if (shape.type === 'fixed') {
41
+ // Fixed-shape: primitives, tuples, or objects with only fixed props
42
+ tools.push(...generateFixedShapeTools(options, shape));
43
+ }
44
+ else if (shape.type === 'dynamic') {
45
+ // Dynamic-shape: arrays or records at root, or objects with only dynamic props
46
+ if (shape.subtype === 'array') {
47
+ tools.push(...generateArrayTools(options, mcpWeb));
48
+ }
49
+ else if (shape.subtype === 'record') {
50
+ tools.push(...generateRecordTools(options, mcpWeb));
51
+ }
52
+ }
53
+ else if (shape.type === 'mixed') {
54
+ // Mixed: objects with both fixed and dynamic props
55
+ tools.push(...generateMixedObjectTools(options, shape, mcpWeb));
56
+ }
57
+ else {
58
+ // Unsupported type
59
+ warnings.push(`⚠️ Warning: Schema for '${name}' contains unsupported types.\n` +
60
+ `Only JSON-compatible types are supported (objects, arrays, records, primitives).`);
61
+ }
62
+ return { tools, warnings };
63
+ }
64
+ // ============================================================================
65
+ // Array Tools Generator
66
+ // ============================================================================
67
+ /**
68
+ * Generates tools for array schemas.
69
+ * Creates 4 tools: get, add, set, delete
70
+ * Uses index-based addressing by default, ID-based when id() marker present.
71
+ */
72
+ function generateArrayTools(options, _mcpWeb) {
73
+ const { name, description, get, set, schema } = options;
74
+ const tools = [];
75
+ const unwrapped = unwrapSchema(schema);
76
+ const elementSchema = unwrapped.element;
77
+ // Check if element is an object with id() marker
78
+ const elementUnwrapped = unwrapSchema(elementSchema);
79
+ let idField = { type: 'none' };
80
+ if (elementUnwrapped instanceof z.ZodObject) {
81
+ idField = findIdField(elementUnwrapped);
82
+ }
83
+ const useIdBased = idField.type === 'explicit';
84
+ // Derive input schemas (exclude system fields)
85
+ let addInputSchema = elementSchema;
86
+ let setInputSchema = elementSchema;
87
+ if (elementUnwrapped instanceof z.ZodObject) {
88
+ addInputSchema = deriveAddInputSchema(elementUnwrapped);
89
+ setInputSchema = deriveSetInputSchema(elementUnwrapped);
90
+ }
91
+ // GET tool
92
+ if (useIdBased) {
93
+ const keyField = idField.field;
94
+ tools.push({
95
+ name: `get_${name}`,
96
+ description: `Get ${description} by ID, or get all if no ID provided`,
97
+ inputSchema: z.object({ id: z.string().optional() }),
98
+ handler: async (input) => {
99
+ const array = get();
100
+ if (input.id !== undefined) {
101
+ return array.find((item) => item[keyField] === input.id);
102
+ }
103
+ return array;
104
+ },
105
+ });
106
+ }
107
+ else {
108
+ tools.push({
109
+ name: `get_${name}`,
110
+ description: `Get ${description} by index, or get all if no index provided`,
111
+ inputSchema: z.object({ index: z.number().int().min(0).optional() }),
112
+ handler: async (input) => {
113
+ const array = get();
114
+ if (input.index !== undefined) {
115
+ return array[input.index];
116
+ }
117
+ return array;
118
+ },
119
+ });
120
+ }
121
+ // ADD tool
122
+ if (useIdBased) {
123
+ tools.push({
124
+ name: `add_${name}`,
125
+ description: `Add a new item to ${description}`,
126
+ inputSchema: z.object({ value: addInputSchema }),
127
+ handler: async (input) => {
128
+ const array = get();
129
+ const parsed = elementSchema.parse(input.value);
130
+ array.push(parsed);
131
+ set(array);
132
+ return { success: true, value: parsed };
133
+ },
134
+ });
135
+ }
136
+ else {
137
+ tools.push({
138
+ name: `add_${name}`,
139
+ description: `Add a new item to ${description} at the specified index (default: end)`,
140
+ inputSchema: z.object({
141
+ value: addInputSchema,
142
+ index: z.number().int().min(0).optional()
143
+ }),
144
+ handler: async (input) => {
145
+ const array = get();
146
+ const parsed = elementSchema.parse(input.value);
147
+ if (input.index !== undefined) {
148
+ array.splice(input.index, 0, parsed);
149
+ }
150
+ else {
151
+ array.push(parsed);
152
+ }
153
+ set(array);
154
+ return { success: true, value: parsed };
155
+ },
156
+ });
157
+ }
158
+ // SET tool (partial update with deep merge)
159
+ if (useIdBased) {
160
+ const keyField = idField.field;
161
+ tools.push({
162
+ name: `set_${name}`,
163
+ description: `Update an item in ${description} by ID (partial update with deep merge)`,
164
+ inputSchema: z.object({
165
+ id: z.string(),
166
+ value: setInputSchema
167
+ }),
168
+ handler: async (input) => {
169
+ const array = get();
170
+ const index = array.findIndex((item) => item[keyField] === input.id);
171
+ if (index === -1) {
172
+ throw new Error(`Item with id '${input.id}' not found in ${name}`);
173
+ }
174
+ const merged = deepMerge(array[index], input.value);
175
+ const validated = elementSchema.parse(merged);
176
+ array[index] = validated;
177
+ set(array);
178
+ return { success: true, value: validated };
179
+ },
180
+ });
181
+ }
182
+ else {
183
+ tools.push({
184
+ name: `set_${name}`,
185
+ description: `Update an item in ${description} by index (partial update with deep merge)`,
186
+ inputSchema: z.object({
187
+ index: z.number().int().min(0),
188
+ value: setInputSchema
189
+ }),
190
+ handler: async (input) => {
191
+ const array = get();
192
+ if (input.index >= array.length) {
193
+ throw new Error(`Index ${input.index} out of bounds for ${name} (length: ${array.length})`);
194
+ }
195
+ const merged = deepMerge(array[input.index], input.value);
196
+ const validated = elementSchema.parse(merged);
197
+ array[input.index] = validated;
198
+ set(array);
199
+ return { success: true, value: validated };
200
+ },
201
+ });
202
+ }
203
+ // DELETE tool
204
+ if (useIdBased) {
205
+ const keyField = idField.field;
206
+ tools.push({
207
+ name: `delete_${name}`,
208
+ description: `Delete an item from ${description} by ID, or delete all items`,
209
+ inputSchema: z.union([
210
+ z.object({ id: z.string() }),
211
+ z.object({ all: z.literal(true) })
212
+ ]),
213
+ handler: async (input) => {
214
+ const array = get();
215
+ if (input.all) {
216
+ set([]);
217
+ }
218
+ else if (input.id) {
219
+ const index = array.findIndex((item) => item[keyField] === input.id);
220
+ if (index !== -1) {
221
+ array.splice(index, 1);
222
+ set(array);
223
+ }
224
+ }
225
+ return { success: true };
226
+ },
227
+ });
228
+ }
229
+ else {
230
+ tools.push({
231
+ name: `delete_${name}`,
232
+ description: `Delete an item from ${description} by index, or delete all items`,
233
+ inputSchema: z.union([
234
+ z.object({ index: z.number().int().min(0) }),
235
+ z.object({ all: z.literal(true) })
236
+ ]),
237
+ handler: async (input) => {
238
+ const array = get();
239
+ if (input.all) {
240
+ set([]);
241
+ }
242
+ else if (input.index !== undefined) {
243
+ if (input.index < array.length) {
244
+ array.splice(input.index, 1);
245
+ set(array);
246
+ }
247
+ }
248
+ return { success: true };
249
+ },
250
+ });
251
+ }
252
+ return tools;
253
+ }
254
+ // ============================================================================
255
+ // Record Tools Generator
256
+ // ============================================================================
257
+ /**
258
+ * Generates tools for record schemas.
259
+ * Creates 3 tools: get, set (upsert), delete
260
+ * Records use string keys naturally, no ID marker needed.
261
+ */
262
+ function generateRecordTools(options, _mcpWeb) {
263
+ const { name, description, get, set, schema } = options;
264
+ const tools = [];
265
+ const unwrapped = unwrapSchema(schema);
266
+ const def = unwrapped._def;
267
+ const valueSchema = def.valueType || z.unknown();
268
+ // Derive input schemas (exclude system fields if value is object)
269
+ const valueUnwrapped = unwrapSchema(valueSchema);
270
+ let setInputSchema = valueSchema;
271
+ if (valueUnwrapped instanceof z.ZodObject) {
272
+ setInputSchema = deriveSetInputSchema(valueUnwrapped);
273
+ }
274
+ // GET tool
275
+ tools.push({
276
+ name: `get_${name}`,
277
+ description: `Get ${description} by key, or get all if no key provided`,
278
+ inputSchema: z.object({ key: z.string().optional() }),
279
+ handler: async (input) => {
280
+ const record = get();
281
+ if (input.key !== undefined) {
282
+ return record[input.key];
283
+ }
284
+ return record;
285
+ },
286
+ });
287
+ // SET tool (upsert: add or update)
288
+ tools.push({
289
+ name: `set_${name}`,
290
+ description: `Set (add or update) an entry in ${description} (partial update with deep merge for objects)`,
291
+ inputSchema: z.object({
292
+ key: z.string(),
293
+ value: setInputSchema
294
+ }),
295
+ handler: async (input) => {
296
+ const record = get();
297
+ // If value schema is object and entry exists, deep merge
298
+ if (valueUnwrapped instanceof z.ZodObject && record[input.key] !== undefined) {
299
+ const merged = deepMerge(record[input.key], input.value);
300
+ const validated = valueSchema.parse(merged);
301
+ record[input.key] = validated;
302
+ set(record);
303
+ return { success: true, value: validated };
304
+ }
305
+ // Otherwise, full replacement (upsert)
306
+ const validated = valueSchema.parse(input.value);
307
+ record[input.key] = validated;
308
+ set(record);
309
+ return { success: true, value: validated };
310
+ },
311
+ });
312
+ // DELETE tool
313
+ tools.push({
314
+ name: `delete_${name}`,
315
+ description: `Delete an entry from ${description} by key, or delete all entries`,
316
+ inputSchema: z.union([
317
+ z.object({ key: z.string() }),
318
+ z.object({ all: z.literal(true) })
319
+ ]),
320
+ handler: async (input) => {
321
+ const record = get();
322
+ if (input.all) {
323
+ set({});
324
+ }
325
+ else if (input.key) {
326
+ delete record[input.key];
327
+ set(record);
328
+ }
329
+ return { success: true };
330
+ },
331
+ });
332
+ return tools;
333
+ }
334
+ // ============================================================================
335
+ // Mixed Object Tools Generator
336
+ // ============================================================================
337
+ /**
338
+ * Generates tools for mixed objects (both fixed and dynamic props).
339
+ * Creates asymmetric get/set:
340
+ * - get() returns full state (including collections)
341
+ * - set() only updates fixed-shape props
342
+ * - Separate tools for each collection
343
+ */
344
+ function generateMixedObjectTools(options, _shape, mcpWeb) {
345
+ const { name, description, get, set, schema } = options;
346
+ const tools = [];
347
+ const unwrapped = unwrapSchema(schema);
348
+ // Split into fixed and dynamic parts
349
+ const fixedShape = {};
350
+ const dynamicFields = [];
351
+ for (const [key, field] of Object.entries(unwrapped.shape)) {
352
+ const zodField = field;
353
+ const unwrappedField = unwrapSchema(zodField);
354
+ if (unwrappedField instanceof z.ZodArray || unwrappedField instanceof z.ZodRecord) {
355
+ dynamicFields.push({ key, field: zodField });
356
+ }
357
+ else {
358
+ fixedShape[key] = zodField;
359
+ }
360
+ }
361
+ const hasFixedProps = Object.keys(fixedShape).length > 0;
362
+ // ROOT GETTER - always returns full state
363
+ tools.push({
364
+ name: `get_${name}`,
365
+ description: `Get the current ${description} (full state including collections)`,
366
+ inputSchema: z.object({ excludeCollections: z.boolean().optional() }),
367
+ handler: async (input) => {
368
+ const fullState = get();
369
+ if (input.excludeCollections) {
370
+ // Return only fixed-shape props
371
+ const fixedState = {};
372
+ for (const key of Object.keys(fixedShape)) {
373
+ fixedState[key] = fullState[key];
374
+ }
375
+ return fixedState;
376
+ }
377
+ return fullState;
378
+ },
379
+ });
380
+ // ROOT SETTER - only if there are fixed props
381
+ if (hasFixedProps) {
382
+ const setInputSchema = deriveSetInputSchema(z.object(fixedShape));
383
+ tools.push({
384
+ name: `set_${name}`,
385
+ description: `Update ${description} settings (fixed-shape props only, use collection tools for arrays/records)`,
386
+ inputSchema: setInputSchema,
387
+ handler: async (input) => {
388
+ const current = get();
389
+ // Merge only the fixed props
390
+ const fixedUpdate = {};
391
+ for (const key of Object.keys(fixedShape)) {
392
+ if (key in input) {
393
+ fixedUpdate[key] = input[key];
394
+ }
395
+ }
396
+ const merged = deepMerge(current, fixedUpdate);
397
+ const validated = schema.parse(merged);
398
+ set(validated);
399
+ // Return only the fixed props that were updated
400
+ const result = {};
401
+ for (const key of Object.keys(fixedShape)) {
402
+ result[key] = validated[key];
403
+ }
404
+ return { success: true, value: result };
405
+ },
406
+ });
407
+ }
408
+ // COLLECTION TOOLS - generate for each dynamic field
409
+ for (const { key, field } of dynamicFields) {
410
+ const unwrappedField = unwrapSchema(field);
411
+ const collectionOptions = {
412
+ name: `${name}_${key}`,
413
+ description: `${key} in ${description}`,
414
+ get: () => get()[key],
415
+ set: (value) => {
416
+ const current = get();
417
+ current[key] = value;
418
+ set(current);
419
+ },
420
+ schema: field,
421
+ };
422
+ if (unwrappedField instanceof z.ZodArray) {
423
+ tools.push(...generateArrayTools(collectionOptions, mcpWeb));
424
+ }
425
+ else if (unwrappedField instanceof z.ZodRecord) {
426
+ tools.push(...generateRecordTools(collectionOptions, mcpWeb));
427
+ }
428
+ }
429
+ return tools;
430
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=tool-generator.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-generator.test.d.ts","sourceRoot":"","sources":["../../src/expanded-schema-tools/tool-generator.test.ts"],"names":[],"mappings":""}