@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,49 @@
1
+ import type { ToolDefinition } from '@mcp-web/types';
2
+ import { z } from 'zod';
3
+ import type { MCPWeb } from './web.js';
4
+ export interface ToolGenerationOptions {
5
+ name: string;
6
+ description: string;
7
+ get: () => unknown;
8
+ set: (value: unknown) => void;
9
+ schema: z.ZodTypeAny;
10
+ }
11
+ export interface GeneratedTools {
12
+ tools: ToolDefinition[];
13
+ warnings: string[];
14
+ }
15
+ export interface SchemaShape {
16
+ type: 'fixed' | 'dynamic' | 'mixed' | 'unsupported';
17
+ subtype: 'object' | 'array' | 'record' | 'primitive' | 'tuple' | 'unknown';
18
+ hasOptionalFields: boolean;
19
+ optionalPaths: string[];
20
+ fixedPaths: string[];
21
+ dynamicPaths: string[];
22
+ }
23
+ export interface KeyFieldResult {
24
+ type: 'explicit' | 'none';
25
+ field?: string;
26
+ }
27
+ /**
28
+ * Generates MCP tools for a schema based on its shape.
29
+ * - Fixed-shape schemas → get + set with deep merge
30
+ * - Dynamic-shape schemas → get + add + set + delete (arrays) or get + set + delete (records)
31
+ * - Mixed schemas → asymmetric get/set + collection tools
32
+ */
33
+ export declare function generateToolsForSchema(options: ToolGenerationOptions, mcpWeb: MCPWeb): GeneratedTools;
34
+ /**
35
+ * Analyzes a schema to determine its shape characteristics.
36
+ */
37
+ export declare function analyzeSchemaShape(schema: z.ZodTypeAny): SchemaShape;
38
+ /**
39
+ * Detects the ID field in an object schema.
40
+ * Returns information about explicit id() markers.
41
+ * Throws an error if multiple id() markers are found.
42
+ */
43
+ export declare function isIdField(schema: z.ZodObject<z.ZodRawShape>): KeyFieldResult;
44
+ /**
45
+ * Validates that a schema only uses supported types.
46
+ * Throws an error if unsupported types are found.
47
+ */
48
+ export declare function validateSupportedTypes(schema: z.ZodTypeAny, path?: string): string[];
49
+ //# sourceMappingURL=zod-to-tools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zod-to-tools.d.ts","sourceRoot":"","sources":["../src/zod-to-tools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACrD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAQxB,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAMvC,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,GAAG,EAAE,MAAM,OAAO,CAAC;IACnB,GAAG,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IAC9B,MAAM,EAAE,CAAC,CAAC,UAAU,CAAC;CACtB;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,OAAO,GAAG,SAAS,GAAG,OAAO,GAAG,aAAa,CAAC;IACpD,OAAO,EAAE,QAAQ,GAAG,OAAO,GAAG,QAAQ,GAAG,WAAW,GAAG,OAAO,GAAG,SAAS,CAAC;IAC3E,iBAAiB,EAAE,OAAO,CAAC;IAC3B,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,UAAU,GAAG,MAAM,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAMD;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,qBAAqB,EAC9B,MAAM,EAAE,MAAM,GACb,cAAc,CAsDhB;AAMD;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,CAAC,CAAC,UAAU,GAAG,WAAW,CAmFpE;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,GAAG,cAAc,CAuB5E;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,CAAC,CAAC,UAAU,EAAE,IAAI,SAAS,GAAG,MAAM,EAAE,CA0CpF"}
@@ -0,0 +1,623 @@
1
+ import { z } from 'zod';
2
+ import { deriveAddInputSchema, deriveSetInputSchema, isKeyField, unwrapSchema, validateSystemFields, } from './schema-helpers.js';
3
+ // ============================================================================
4
+ // Main Entry Point
5
+ // ============================================================================
6
+ /**
7
+ * Generates MCP tools for a schema based on its shape.
8
+ * - Fixed-shape schemas → get + set with deep merge
9
+ * - Dynamic-shape schemas → get + add + set + delete (arrays) or get + set + delete (records)
10
+ * - Mixed schemas → asymmetric get/set + collection tools
11
+ */
12
+ export function generateToolsForSchema(options, mcpWeb) {
13
+ const { name, schema } = options;
14
+ const tools = [];
15
+ const warnings = [];
16
+ // Validate schema
17
+ const unwrapped = unwrapSchema(schema);
18
+ // Validate system fields if it's an object
19
+ if (unwrapped instanceof z.ZodObject) {
20
+ validateSystemFields(unwrapped);
21
+ }
22
+ // Analyze schema shape
23
+ const shape = analyzeSchemaShape(schema);
24
+ // Warn about optional fields (once)
25
+ if (shape.hasOptionalFields) {
26
+ warnings.push(`⚠️ Warning: Schema for '${name}' uses optional() on field(s): ${shape.optionalPaths.join(', ')}.\n\n` +
27
+ `Problem: optional() creates ambiguity in partial updates:\n` +
28
+ `- Is the field missing because AI didn't provide it? (keep current)\n` +
29
+ `- Or because AI wants to clear it? (set to undefined)\n\n` +
30
+ `Solution: Use nullable() instead:\n` +
31
+ `- fieldName: z.string().nullable()\n\n` +
32
+ `This makes intent explicit:\n` +
33
+ `- Omit field → keep current value\n` +
34
+ `- Pass null → clear the value`);
35
+ }
36
+ // Generate tools based on schema type
37
+ if (shape.type === 'fixed') {
38
+ // Fixed-shape: primitives, tuples, or objects with only fixed props
39
+ tools.push(...generateFixedShapeTools(options, shape, mcpWeb));
40
+ }
41
+ else if (shape.type === 'dynamic') {
42
+ // Dynamic-shape: arrays or records at root, or objects with only dynamic props
43
+ if (shape.subtype === 'array') {
44
+ tools.push(...generateArrayTools(options, mcpWeb));
45
+ }
46
+ else if (shape.subtype === 'record') {
47
+ tools.push(...generateRecordTools(options, mcpWeb));
48
+ }
49
+ }
50
+ else if (shape.type === 'mixed') {
51
+ // Mixed: objects with both fixed and dynamic props
52
+ tools.push(...generateMixedObjectTools(options, shape, mcpWeb));
53
+ }
54
+ else {
55
+ // Unsupported type
56
+ warnings.push(`⚠️ Warning: Schema for '${name}' contains unsupported types.\n` +
57
+ `Only JSON-compatible types are supported (objects, arrays, records, primitives).`);
58
+ }
59
+ return { tools, warnings };
60
+ }
61
+ // ============================================================================
62
+ // Schema Analysis
63
+ // ============================================================================
64
+ /**
65
+ * Analyzes a schema to determine its shape characteristics.
66
+ */
67
+ export function analyzeSchemaShape(schema) {
68
+ const unwrapped = unwrapSchema(schema);
69
+ // Default result
70
+ const result = {
71
+ type: 'unsupported',
72
+ subtype: 'unknown',
73
+ hasOptionalFields: false,
74
+ optionalPaths: [],
75
+ fixedPaths: [],
76
+ dynamicPaths: [],
77
+ };
78
+ // Primitives
79
+ if (unwrapped instanceof z.ZodString ||
80
+ unwrapped instanceof z.ZodNumber ||
81
+ unwrapped instanceof z.ZodBoolean ||
82
+ unwrapped instanceof z.ZodLiteral ||
83
+ unwrapped instanceof z.ZodEnum) {
84
+ result.type = 'fixed';
85
+ result.subtype = 'primitive';
86
+ return result;
87
+ }
88
+ // Tuples
89
+ if (unwrapped instanceof z.ZodTuple) {
90
+ result.type = 'fixed';
91
+ result.subtype = 'tuple';
92
+ return result;
93
+ }
94
+ // Arrays
95
+ if (unwrapped instanceof z.ZodArray) {
96
+ result.type = 'dynamic';
97
+ result.subtype = 'array';
98
+ return result;
99
+ }
100
+ // Records
101
+ if (unwrapped instanceof z.ZodRecord) {
102
+ result.type = 'dynamic';
103
+ result.subtype = 'record';
104
+ return result;
105
+ }
106
+ // Objects - analyze further
107
+ if (unwrapped instanceof z.ZodObject) {
108
+ result.subtype = 'object';
109
+ const shape = unwrapped.shape;
110
+ for (const [key, field] of Object.entries(shape)) {
111
+ const zodField = field;
112
+ const unwrappedField = unwrapSchema(zodField);
113
+ // Check for optional fields
114
+ if (zodField instanceof z.ZodOptional) {
115
+ result.hasOptionalFields = true;
116
+ result.optionalPaths.push(key);
117
+ }
118
+ // Check if field is dynamic (array or record)
119
+ if (unwrappedField instanceof z.ZodArray || unwrappedField instanceof z.ZodRecord) {
120
+ result.dynamicPaths.push(key);
121
+ }
122
+ else {
123
+ result.fixedPaths.push(key);
124
+ }
125
+ }
126
+ // Determine object type
127
+ if (result.dynamicPaths.length > 0 && result.fixedPaths.length > 0) {
128
+ result.type = 'mixed';
129
+ }
130
+ else if (result.dynamicPaths.length > 0) {
131
+ result.type = 'dynamic';
132
+ }
133
+ else {
134
+ result.type = 'fixed';
135
+ }
136
+ return result;
137
+ }
138
+ return result;
139
+ }
140
+ /**
141
+ * Detects the ID field in an object schema.
142
+ * Returns information about explicit id() markers.
143
+ * Throws an error if multiple id() markers are found.
144
+ */
145
+ export function isIdField(schema) {
146
+ const shape = schema.shape;
147
+ let explicitKey = null;
148
+ for (const [name, field] of Object.entries(shape)) {
149
+ if (isKeyField(field)) {
150
+ // Error if multiple id() markers
151
+ if (explicitKey) {
152
+ throw new Error(`Multiple fields marked with id(): '${explicitKey}' and '${name}'.\n` +
153
+ `Only one field can be the ID. For compound keys, use index-based addressing.`);
154
+ }
155
+ explicitKey = name;
156
+ }
157
+ }
158
+ if (explicitKey) {
159
+ return { type: 'explicit', field: explicitKey };
160
+ }
161
+ // No id() marker → use index-based addressing
162
+ return { type: 'none' };
163
+ }
164
+ /**
165
+ * Validates that a schema only uses supported types.
166
+ * Throws an error if unsupported types are found.
167
+ */
168
+ export function validateSupportedTypes(schema, path = 'root') {
169
+ const errors = [];
170
+ const unwrapped = unwrapSchema(schema);
171
+ const supported = [
172
+ z.ZodObject,
173
+ z.ZodArray,
174
+ z.ZodRecord,
175
+ z.ZodString,
176
+ z.ZodNumber,
177
+ z.ZodBoolean,
178
+ z.ZodLiteral,
179
+ z.ZodEnum,
180
+ z.ZodTuple,
181
+ z.ZodDate,
182
+ z.ZodBigInt,
183
+ z.ZodNull,
184
+ z.ZodUndefined,
185
+ ];
186
+ const isSupported = supported.some((type) => unwrapped instanceof type);
187
+ if (!isSupported) {
188
+ errors.push(`Unsupported type at '${path}': ${unwrapped.constructor.name}`);
189
+ }
190
+ // Recurse for nested schemas
191
+ if (unwrapped instanceof z.ZodObject) {
192
+ for (const [key, field] of Object.entries(unwrapped.shape)) {
193
+ errors.push(...validateSupportedTypes(field, `${path}.${key}`));
194
+ }
195
+ }
196
+ else if (unwrapped instanceof z.ZodArray) {
197
+ errors.push(...validateSupportedTypes(unwrapped.element, `${path}[]`));
198
+ }
199
+ else if (unwrapped instanceof z.ZodRecord) {
200
+ const def = unwrapped._def;
201
+ const valueType = def.valueType;
202
+ if (valueType) {
203
+ errors.push(...validateSupportedTypes(valueType, `${path}{}`));
204
+ }
205
+ }
206
+ return errors;
207
+ }
208
+ // ============================================================================
209
+ // Tool Generators
210
+ // ============================================================================
211
+ /**
212
+ * Generates getter and setter tools for fixed-shape schemas.
213
+ * Fixed-shape includes: primitives, tuples, and objects with only fixed props.
214
+ */
215
+ function generateFixedShapeTools(options, shape, _mcpWeb) {
216
+ const { name, description, get, set, schema } = options;
217
+ const tools = [];
218
+ const unwrapped = unwrapSchema(schema);
219
+ // Getter tool
220
+ tools.push({
221
+ name: `get_${name}`,
222
+ description: `Get the current ${description}`,
223
+ inputSchema: z.object({}),
224
+ handler: async () => {
225
+ return get();
226
+ },
227
+ });
228
+ // Setter tool - behavior depends on subtype
229
+ if (shape.subtype === 'primitive' || shape.subtype === 'tuple') {
230
+ // Full replacement for primitives and tuples
231
+ tools.push({
232
+ name: `set_${name}`,
233
+ description: `Set the ${description}`,
234
+ inputSchema: z.object({ value: schema }),
235
+ handler: async (input) => {
236
+ const validated = schema.parse(input.value);
237
+ set(validated);
238
+ return { success: true, value: validated };
239
+ },
240
+ });
241
+ }
242
+ else if (shape.subtype === 'object') {
243
+ // Partial update with deep merge for objects
244
+ const objectSchema = unwrapped;
245
+ const setInputSchema = deriveSetInputSchema(objectSchema);
246
+ tools.push({
247
+ name: `set_${name}`,
248
+ description: `Update the ${description} (partial update with deep merge)`,
249
+ inputSchema: setInputSchema,
250
+ handler: async (input) => {
251
+ const current = get();
252
+ const { deepMerge } = await import('./utils.js');
253
+ const merged = deepMerge(current, input);
254
+ const validated = schema.parse(merged);
255
+ set(validated);
256
+ return { success: true, value: validated };
257
+ },
258
+ });
259
+ }
260
+ return tools;
261
+ }
262
+ /**
263
+ * Generates tools for array schemas.
264
+ * Creates 4 tools: get, add, set, delete
265
+ * Uses index-based addressing by default, ID-based when id() marker present.
266
+ */
267
+ function generateArrayTools(options, _mcpWeb) {
268
+ const { name, description, get, set, schema } = options;
269
+ const tools = [];
270
+ const unwrapped = unwrapSchema(schema);
271
+ const elementSchema = unwrapped.element;
272
+ // Check if element is an object with id() marker
273
+ const elementUnwrapped = unwrapSchema(elementSchema);
274
+ let idField = { type: 'none' };
275
+ if (elementUnwrapped instanceof z.ZodObject) {
276
+ idField = isIdField(elementUnwrapped);
277
+ }
278
+ const useIdBased = idField.type === 'explicit';
279
+ // Derive input schemas (exclude system fields)
280
+ let addInputSchema = elementSchema;
281
+ let setInputSchema = elementSchema;
282
+ if (elementUnwrapped instanceof z.ZodObject) {
283
+ addInputSchema = deriveAddInputSchema(elementUnwrapped);
284
+ setInputSchema = deriveSetInputSchema(elementUnwrapped);
285
+ }
286
+ // GET tool
287
+ if (useIdBased) {
288
+ const keyField = idField.field; // Safe because useIdBased checks type === 'explicit'
289
+ tools.push({
290
+ name: `get_${name}`,
291
+ description: `Get ${description} by ID, or get all if no ID provided`,
292
+ inputSchema: z.object({ id: z.string().optional() }),
293
+ handler: async (input) => {
294
+ const array = get();
295
+ if (input.id !== undefined) {
296
+ return array.find((item) => item[keyField] === input.id);
297
+ }
298
+ return array;
299
+ },
300
+ });
301
+ }
302
+ else {
303
+ tools.push({
304
+ name: `get_${name}`,
305
+ description: `Get ${description} by index, or get all if no index provided`,
306
+ inputSchema: z.object({ index: z.number().int().min(0).optional() }),
307
+ handler: async (input) => {
308
+ const array = get();
309
+ if (input.index !== undefined) {
310
+ return array[input.index];
311
+ }
312
+ return array;
313
+ },
314
+ });
315
+ }
316
+ // ADD tool
317
+ if (useIdBased) {
318
+ tools.push({
319
+ name: `add_${name}`,
320
+ description: `Add a new item to ${description}`,
321
+ inputSchema: z.object({ value: addInputSchema }),
322
+ handler: async (input) => {
323
+ const array = get();
324
+ const parsed = elementSchema.parse(input.value);
325
+ array.push(parsed);
326
+ set(array);
327
+ return { success: true, value: parsed };
328
+ },
329
+ });
330
+ }
331
+ else {
332
+ tools.push({
333
+ name: `add_${name}`,
334
+ description: `Add a new item to ${description} at the specified index (default: end)`,
335
+ inputSchema: z.object({
336
+ value: addInputSchema,
337
+ index: z.number().int().min(0).optional()
338
+ }),
339
+ handler: async (input) => {
340
+ const array = get();
341
+ const parsed = elementSchema.parse(input.value);
342
+ if (input.index !== undefined) {
343
+ array.splice(input.index, 0, parsed);
344
+ }
345
+ else {
346
+ array.push(parsed);
347
+ }
348
+ set(array);
349
+ return { success: true, value: parsed };
350
+ },
351
+ });
352
+ }
353
+ // SET tool (partial update with deep merge)
354
+ if (useIdBased) {
355
+ const keyField = idField.field; // Safe because useIdBased checks type === 'explicit'
356
+ tools.push({
357
+ name: `set_${name}`,
358
+ description: `Update an item in ${description} by ID (partial update with deep merge)`,
359
+ inputSchema: z.object({
360
+ id: z.string(),
361
+ value: setInputSchema
362
+ }),
363
+ handler: async (input) => {
364
+ const array = get();
365
+ const index = array.findIndex((item) => item[keyField] === input.id);
366
+ if (index === -1) {
367
+ throw new Error(`Item with id '${input.id}' not found in ${name}`);
368
+ }
369
+ const { deepMerge } = await import('./utils.js');
370
+ const merged = deepMerge(array[index], input.value);
371
+ const validated = elementSchema.parse(merged);
372
+ array[index] = validated;
373
+ set(array);
374
+ return { success: true, value: validated };
375
+ },
376
+ });
377
+ }
378
+ else {
379
+ tools.push({
380
+ name: `set_${name}`,
381
+ description: `Update an item in ${description} by index (partial update with deep merge)`,
382
+ inputSchema: z.object({
383
+ index: z.number().int().min(0),
384
+ value: setInputSchema
385
+ }),
386
+ handler: async (input) => {
387
+ const array = get();
388
+ if (input.index >= array.length) {
389
+ throw new Error(`Index ${input.index} out of bounds for ${name} (length: ${array.length})`);
390
+ }
391
+ const { deepMerge } = await import('./utils.js');
392
+ const merged = deepMerge(array[input.index], input.value);
393
+ const validated = elementSchema.parse(merged);
394
+ array[input.index] = validated;
395
+ set(array);
396
+ return { success: true, value: validated };
397
+ },
398
+ });
399
+ }
400
+ // DELETE tool
401
+ if (useIdBased) {
402
+ const keyField = idField.field; // Safe because useIdBased checks type === 'explicit'
403
+ tools.push({
404
+ name: `delete_${name}`,
405
+ description: `Delete an item from ${description} by ID, or delete all items`,
406
+ inputSchema: z.union([
407
+ z.object({ id: z.string() }),
408
+ z.object({ all: z.literal(true) })
409
+ ]),
410
+ handler: async (input) => {
411
+ const array = get();
412
+ if (input.all) {
413
+ set([]);
414
+ }
415
+ else if (input.id) {
416
+ const index = array.findIndex((item) => item[keyField] === input.id);
417
+ if (index !== -1) {
418
+ array.splice(index, 1);
419
+ set(array);
420
+ }
421
+ }
422
+ return { success: true };
423
+ },
424
+ });
425
+ }
426
+ else {
427
+ tools.push({
428
+ name: `delete_${name}`,
429
+ description: `Delete an item from ${description} by index, or delete all items`,
430
+ inputSchema: z.union([
431
+ z.object({ index: z.number().int().min(0) }),
432
+ z.object({ all: z.literal(true) })
433
+ ]),
434
+ handler: async (input) => {
435
+ const array = get();
436
+ if (input.all) {
437
+ set([]);
438
+ }
439
+ else if (input.index !== undefined) {
440
+ if (input.index < array.length) {
441
+ array.splice(input.index, 1);
442
+ set(array);
443
+ }
444
+ }
445
+ return { success: true };
446
+ },
447
+ });
448
+ }
449
+ return tools;
450
+ }
451
+ /**
452
+ * Generates tools for record schemas.
453
+ * Creates 3 tools: get, set (upsert), delete
454
+ * Records use string keys naturally, no ID marker needed.
455
+ */
456
+ function generateRecordTools(options, _mcpWeb) {
457
+ const { name, description, get, set, schema } = options;
458
+ const tools = [];
459
+ const unwrapped = unwrapSchema(schema);
460
+ const def = unwrapped._def;
461
+ const valueSchema = def.valueType || z.unknown();
462
+ // Derive input schemas (exclude system fields if value is object)
463
+ const valueUnwrapped = unwrapSchema(valueSchema);
464
+ let setInputSchema = valueSchema;
465
+ if (valueUnwrapped instanceof z.ZodObject) {
466
+ setInputSchema = deriveSetInputSchema(valueUnwrapped);
467
+ }
468
+ // GET tool
469
+ tools.push({
470
+ name: `get_${name}`,
471
+ description: `Get ${description} by key, or get all if no key provided`,
472
+ inputSchema: z.object({ key: z.string().optional() }),
473
+ handler: async (input) => {
474
+ const record = get();
475
+ if (input.key !== undefined) {
476
+ return record[input.key];
477
+ }
478
+ return record;
479
+ },
480
+ });
481
+ // SET tool (upsert: add or update)
482
+ tools.push({
483
+ name: `set_${name}`,
484
+ description: `Set (add or update) an entry in ${description} (partial update with deep merge for objects)`,
485
+ inputSchema: z.object({
486
+ key: z.string(),
487
+ value: setInputSchema
488
+ }),
489
+ handler: async (input) => {
490
+ const record = get();
491
+ // If value schema is object and entry exists, deep merge
492
+ if (valueUnwrapped instanceof z.ZodObject && record[input.key] !== undefined) {
493
+ const { deepMerge } = await import('./utils.js');
494
+ const merged = deepMerge(record[input.key], input.value);
495
+ const validated = valueSchema.parse(merged);
496
+ record[input.key] = validated;
497
+ set(record);
498
+ return { success: true, value: validated };
499
+ }
500
+ // Otherwise, full replacement (upsert)
501
+ const validated = valueSchema.parse(input.value);
502
+ record[input.key] = validated;
503
+ set(record);
504
+ return { success: true, value: validated };
505
+ },
506
+ });
507
+ // DELETE tool
508
+ tools.push({
509
+ name: `delete_${name}`,
510
+ description: `Delete an entry from ${description} by key, or delete all entries`,
511
+ inputSchema: z.union([
512
+ z.object({ key: z.string() }),
513
+ z.object({ all: z.literal(true) })
514
+ ]),
515
+ handler: async (input) => {
516
+ const record = get();
517
+ if (input.all) {
518
+ set({});
519
+ }
520
+ else if (input.key) {
521
+ delete record[input.key];
522
+ set(record);
523
+ }
524
+ return { success: true };
525
+ },
526
+ });
527
+ return tools;
528
+ }
529
+ /**
530
+ * Generates tools for mixed objects (both fixed and dynamic props).
531
+ * Creates asymmetric get/set:
532
+ * - get() returns full state (including collections)
533
+ * - set() only updates fixed-shape props
534
+ * - Separate tools for each collection
535
+ */
536
+ function generateMixedObjectTools(options, _shape, mcpWeb) {
537
+ const { name, description, get, set, schema } = options;
538
+ const tools = [];
539
+ const unwrapped = unwrapSchema(schema);
540
+ // Split into fixed and dynamic parts
541
+ const fixedShape = {};
542
+ const dynamicFields = [];
543
+ for (const [key, field] of Object.entries(unwrapped.shape)) {
544
+ const zodField = field;
545
+ const unwrappedField = unwrapSchema(zodField);
546
+ if (unwrappedField instanceof z.ZodArray || unwrappedField instanceof z.ZodRecord) {
547
+ dynamicFields.push({ key, field: zodField });
548
+ }
549
+ else {
550
+ fixedShape[key] = zodField;
551
+ }
552
+ }
553
+ const hasFixedProps = Object.keys(fixedShape).length > 0;
554
+ // ROOT GETTER - always returns full state
555
+ tools.push({
556
+ name: `get_${name}`,
557
+ description: `Get the current ${description} (full state including collections)`,
558
+ inputSchema: z.object({ excludeCollections: z.boolean().optional() }),
559
+ handler: async (input) => {
560
+ const fullState = get();
561
+ if (input.excludeCollections) {
562
+ // Return only fixed-shape props
563
+ const fixedState = {};
564
+ for (const key of Object.keys(fixedShape)) {
565
+ fixedState[key] = fullState[key];
566
+ }
567
+ return fixedState;
568
+ }
569
+ return fullState;
570
+ },
571
+ });
572
+ // ROOT SETTER - only if there are fixed props
573
+ if (hasFixedProps) {
574
+ const setInputSchema = deriveSetInputSchema(z.object(fixedShape));
575
+ tools.push({
576
+ name: `set_${name}`,
577
+ description: `Update ${description} settings (fixed-shape props only, use collection tools for arrays/records)`,
578
+ inputSchema: setInputSchema,
579
+ handler: async (input) => {
580
+ const current = get();
581
+ const { deepMerge } = await import('./utils.js');
582
+ // Merge only the fixed props
583
+ const fixedUpdate = {};
584
+ for (const key of Object.keys(fixedShape)) {
585
+ if (key in input) {
586
+ fixedUpdate[key] = input[key];
587
+ }
588
+ }
589
+ const merged = deepMerge(current, fixedUpdate);
590
+ const validated = schema.parse(merged);
591
+ set(validated);
592
+ // Return only the fixed props that were updated
593
+ const result = {};
594
+ for (const key of Object.keys(fixedShape)) {
595
+ result[key] = validated[key];
596
+ }
597
+ return { success: true, value: result };
598
+ },
599
+ });
600
+ }
601
+ // COLLECTION TOOLS - generate for each dynamic field
602
+ for (const { key, field } of dynamicFields) {
603
+ const unwrappedField = unwrapSchema(field);
604
+ const collectionOptions = {
605
+ name: `${name}_${key}`,
606
+ description: `${key} in ${description}`,
607
+ get: () => get()[key],
608
+ set: (value) => {
609
+ const current = get();
610
+ current[key] = value;
611
+ set(current);
612
+ },
613
+ schema: field,
614
+ };
615
+ if (unwrappedField instanceof z.ZodArray) {
616
+ tools.push(...generateArrayTools(collectionOptions, mcpWeb));
617
+ }
618
+ else if (unwrappedField instanceof z.ZodRecord) {
619
+ tools.push(...generateRecordTools(collectionOptions, mcpWeb));
620
+ }
621
+ }
622
+ return tools;
623
+ }