@mcp-web/core 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/web.d.ts +1 -1
- package/dist/web.js +1 -1
- package/package.json +9 -4
- package/dist/expanded-schema-tools/generate-fixed-shape-tools.d.ts +0 -8
- package/dist/expanded-schema-tools/generate-fixed-shape-tools.d.ts.map +0 -1
- package/dist/expanded-schema-tools/generate-fixed-shape-tools.js +0 -53
- package/dist/expanded-schema-tools/generate-fixed-shape-tools.test.d.ts +0 -2
- package/dist/expanded-schema-tools/generate-fixed-shape-tools.test.d.ts.map +0 -1
- package/dist/expanded-schema-tools/generate-fixed-shape-tools.test.js +0 -331
- package/dist/expanded-schema-tools/index.d.ts +0 -4
- package/dist/expanded-schema-tools/index.d.ts.map +0 -1
- package/dist/expanded-schema-tools/index.js +0 -2
- package/dist/expanded-schema-tools/integration.test.d.ts +0 -2
- package/dist/expanded-schema-tools/integration.test.d.ts.map +0 -1
- package/dist/expanded-schema-tools/integration.test.js +0 -599
- package/dist/expanded-schema-tools/schema-analysis.d.ts +0 -18
- package/dist/expanded-schema-tools/schema-analysis.d.ts.map +0 -1
- package/dist/expanded-schema-tools/schema-analysis.js +0 -142
- package/dist/expanded-schema-tools/schema-analysis.test.d.ts +0 -2
- package/dist/expanded-schema-tools/schema-analysis.test.d.ts.map +0 -1
- package/dist/expanded-schema-tools/schema-analysis.test.js +0 -314
- package/dist/expanded-schema-tools/schema-helpers.d.ts +0 -69
- package/dist/expanded-schema-tools/schema-helpers.d.ts.map +0 -1
- package/dist/expanded-schema-tools/schema-helpers.js +0 -139
- package/dist/expanded-schema-tools/schema-helpers.test.d.ts +0 -2
- package/dist/expanded-schema-tools/schema-helpers.test.d.ts.map +0 -1
- package/dist/expanded-schema-tools/schema-helpers.test.js +0 -223
- package/dist/expanded-schema-tools/tool-generator.d.ts +0 -10
- package/dist/expanded-schema-tools/tool-generator.d.ts.map +0 -1
- package/dist/expanded-schema-tools/tool-generator.js +0 -430
- package/dist/expanded-schema-tools/tool-generator.test.d.ts +0 -2
- package/dist/expanded-schema-tools/tool-generator.test.d.ts.map +0 -1
- package/dist/expanded-schema-tools/tool-generator.test.js +0 -689
- package/dist/expanded-schema-tools/types.d.ts +0 -26
- package/dist/expanded-schema-tools/types.d.ts.map +0 -1
- package/dist/expanded-schema-tools/types.js +0 -1
- package/dist/expanded-schema-tools/utils.d.ts +0 -16
- package/dist/expanded-schema-tools/utils.d.ts.map +0 -1
- package/dist/expanded-schema-tools/utils.js +0 -35
- package/dist/expanded-schema-tools/utils.test.d.ts +0 -2
- package/dist/expanded-schema-tools/utils.test.d.ts.map +0 -1
- package/dist/expanded-schema-tools/utils.test.js +0 -169
- package/dist/schema-helpers.d.ts +0 -69
- package/dist/schema-helpers.d.ts.map +0 -1
- package/dist/schema-helpers.js +0 -139
- package/dist/zod-to-tools.d.ts +0 -49
- package/dist/zod-to-tools.d.ts.map +0 -1
- package/dist/zod-to-tools.js +0 -623
|
@@ -1,223 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,10 +0,0 @@
|
|
|
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
|
|
@@ -1 +0,0 @@
|
|
|
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"}
|
|
@@ -1,430 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"tool-generator.test.d.ts","sourceRoot":"","sources":["../../src/expanded-schema-tools/tool-generator.test.ts"],"names":[],"mappings":""}
|