@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.
- package/LICENSE +201 -0
- package/README.md +253 -0
- package/dist/addTool.typetest.d.ts +11 -0
- package/dist/addTool.typetest.d.ts.map +1 -0
- package/dist/addTool.typetest.js +248 -0
- package/dist/create-state-tools.d.ts +77 -0
- package/dist/create-state-tools.d.ts.map +1 -0
- package/dist/create-state-tools.js +181 -0
- package/dist/create-tool.d.ts +90 -0
- package/dist/create-tool.d.ts.map +1 -0
- package/dist/create-tool.js +82 -0
- package/dist/expanded-schema-tools/generate-fixed-shape-tools.d.ts +8 -0
- package/dist/expanded-schema-tools/generate-fixed-shape-tools.d.ts.map +1 -0
- package/dist/expanded-schema-tools/generate-fixed-shape-tools.js +53 -0
- package/dist/expanded-schema-tools/generate-fixed-shape-tools.test.d.ts +2 -0
- package/dist/expanded-schema-tools/generate-fixed-shape-tools.test.d.ts.map +1 -0
- package/dist/expanded-schema-tools/generate-fixed-shape-tools.test.js +331 -0
- package/dist/expanded-schema-tools/index.d.ts +4 -0
- package/dist/expanded-schema-tools/index.d.ts.map +1 -0
- package/dist/expanded-schema-tools/index.js +2 -0
- package/dist/expanded-schema-tools/integration.test.d.ts +2 -0
- package/dist/expanded-schema-tools/integration.test.d.ts.map +1 -0
- package/dist/expanded-schema-tools/integration.test.js +599 -0
- package/dist/expanded-schema-tools/schema-analysis.d.ts +18 -0
- package/dist/expanded-schema-tools/schema-analysis.d.ts.map +1 -0
- package/dist/expanded-schema-tools/schema-analysis.js +142 -0
- package/dist/expanded-schema-tools/schema-analysis.test.d.ts +2 -0
- package/dist/expanded-schema-tools/schema-analysis.test.d.ts.map +1 -0
- package/dist/expanded-schema-tools/schema-analysis.test.js +314 -0
- package/dist/expanded-schema-tools/schema-helpers.d.ts +69 -0
- package/dist/expanded-schema-tools/schema-helpers.d.ts.map +1 -0
- package/dist/expanded-schema-tools/schema-helpers.js +139 -0
- package/dist/expanded-schema-tools/schema-helpers.test.d.ts +2 -0
- package/dist/expanded-schema-tools/schema-helpers.test.d.ts.map +1 -0
- package/dist/expanded-schema-tools/schema-helpers.test.js +223 -0
- package/dist/expanded-schema-tools/tool-generator.d.ts +10 -0
- package/dist/expanded-schema-tools/tool-generator.d.ts.map +1 -0
- package/dist/expanded-schema-tools/tool-generator.js +430 -0
- package/dist/expanded-schema-tools/tool-generator.test.d.ts +2 -0
- package/dist/expanded-schema-tools/tool-generator.test.d.ts.map +1 -0
- package/dist/expanded-schema-tools/tool-generator.test.js +689 -0
- package/dist/expanded-schema-tools/types.d.ts +26 -0
- package/dist/expanded-schema-tools/types.d.ts.map +1 -0
- package/dist/expanded-schema-tools/types.js +1 -0
- package/dist/expanded-schema-tools/utils.d.ts +16 -0
- package/dist/expanded-schema-tools/utils.d.ts.map +1 -0
- package/dist/expanded-schema-tools/utils.js +35 -0
- package/dist/expanded-schema-tools/utils.test.d.ts +2 -0
- package/dist/expanded-schema-tools/utils.test.d.ts.map +1 -0
- package/dist/expanded-schema-tools/utils.test.js +169 -0
- package/dist/group-state.d.ts +60 -0
- package/dist/group-state.d.ts.map +1 -0
- package/dist/group-state.js +54 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/query.d.ts +104 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +128 -0
- package/dist/schema-helpers.d.ts +69 -0
- package/dist/schema-helpers.d.ts.map +1 -0
- package/dist/schema-helpers.js +139 -0
- package/dist/schemas.d.ts +140 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +70 -0
- package/dist/tool-generators/generate-basic-state-tools.d.ts +23 -0
- package/dist/tool-generators/generate-basic-state-tools.d.ts.map +1 -0
- package/dist/tool-generators/generate-basic-state-tools.js +95 -0
- package/dist/tool-generators/generate-fixed-shape-tools.d.ts +8 -0
- package/dist/tool-generators/generate-fixed-shape-tools.d.ts.map +1 -0
- package/dist/tool-generators/generate-fixed-shape-tools.js +53 -0
- package/dist/tool-generators/index.d.ts +6 -0
- package/dist/tool-generators/index.d.ts.map +1 -0
- package/dist/tool-generators/index.js +3 -0
- package/dist/tool-generators/schema-analysis.d.ts +18 -0
- package/dist/tool-generators/schema-analysis.d.ts.map +1 -0
- package/dist/tool-generators/schema-analysis.js +142 -0
- package/dist/tool-generators/schema-helpers.d.ts +87 -0
- package/dist/tool-generators/schema-helpers.d.ts.map +1 -0
- package/dist/tool-generators/schema-helpers.js +157 -0
- package/dist/tool-generators/tool-generator.d.ts +11 -0
- package/dist/tool-generators/tool-generator.d.ts.map +1 -0
- package/dist/tool-generators/tool-generator.js +437 -0
- package/dist/tool-generators/types.d.ts +26 -0
- package/dist/tool-generators/types.d.ts.map +1 -0
- package/dist/tool-generators/types.js +1 -0
- package/dist/tool-generators/utils.d.ts +16 -0
- package/dist/tool-generators/utils.d.ts.map +1 -0
- package/dist/tool-generators/utils.js +35 -0
- package/dist/types.d.ts +17 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +31 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +108 -0
- package/dist/web.d.ts +680 -0
- package/dist/web.d.ts.map +1 -0
- package/dist/web.js +1312 -0
- package/dist/zod-to-tools.d.ts +49 -0
- package/dist/zod-to-tools.d.ts.map +1 -0
- package/dist/zod-to-tools.js +623 -0
- 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
|
+
}
|