@tooluminati/forms 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 +21 -0
- package/dist/create-form-tools.d.ts +6 -0
- package/dist/create-form-tools.d.ts.map +1 -0
- package/dist/create-form-tools.js +96 -0
- package/dist/create-form-tools.test.d.ts +2 -0
- package/dist/create-form-tools.test.d.ts.map +1 -0
- package/dist/create-form-tools.test.js +45 -0
- package/dist/errors.d.ts +7 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +9 -0
- package/dist/form-tool-definition.d.ts +11 -0
- package/dist/form-tool-definition.d.ts.map +1 -0
- package/dist/form-tool-definition.js +1 -0
- package/dist/formik.d.ts +16 -0
- package/dist/formik.d.ts.map +1 -0
- package/dist/formik.js +35 -0
- package/dist/index.cjs +413 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +375 -0
- package/dist/infer-schema.d.ts +3 -0
- package/dist/infer-schema.d.ts.map +1 -0
- package/dist/infer-schema.js +42 -0
- package/dist/infer-schema.test.d.ts +2 -0
- package/dist/infer-schema.test.d.ts.map +1 -0
- package/dist/infer-schema.test.js +27 -0
- package/dist/native-form.d.ts +8 -0
- package/dist/native-form.d.ts.map +1 -0
- package/dist/native-form.js +41 -0
- package/dist/react-hook-form.d.ts +21 -0
- package/dist/react-hook-form.d.ts.map +1 -0
- package/dist/react-hook-form.js +53 -0
- package/dist/react-hook-form.test.d.ts +2 -0
- package/dist/react-hook-form.test.d.ts.map +1 -0
- package/dist/react-hook-form.test.js +28 -0
- package/dist/tanstack-form.d.ts +17 -0
- package/dist/tanstack-form.d.ts.map +1 -0
- package/dist/tanstack-form.js +33 -0
- package/dist/types.d.ts +41 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/useWebMcpFormTool.d.ts +5 -0
- package/dist/useWebMcpFormTool.d.ts.map +1 -0
- package/dist/useWebMcpFormTool.js +14 -0
- package/package.json +54 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var FormValidationError = class extends Error {
|
|
3
|
+
errors;
|
|
4
|
+
constructor(message, errors) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "FormValidationError";
|
|
7
|
+
this.errors = errors;
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// src/useWebMcpFormTool.ts
|
|
12
|
+
import { useMemo } from "react";
|
|
13
|
+
import { useWebMcpTools } from "@tooluminati/react";
|
|
14
|
+
|
|
15
|
+
// src/infer-schema.ts
|
|
16
|
+
function inferSchemaFromValue(value) {
|
|
17
|
+
if (typeof value === "string") {
|
|
18
|
+
return { type: "string" };
|
|
19
|
+
}
|
|
20
|
+
if (typeof value === "number") {
|
|
21
|
+
return { type: "number" };
|
|
22
|
+
}
|
|
23
|
+
if (typeof value === "boolean") {
|
|
24
|
+
return { type: "boolean" };
|
|
25
|
+
}
|
|
26
|
+
if (value === null || value === void 0) {
|
|
27
|
+
return void 0;
|
|
28
|
+
}
|
|
29
|
+
if (Array.isArray(value)) {
|
|
30
|
+
if (value.length === 0) {
|
|
31
|
+
return void 0;
|
|
32
|
+
}
|
|
33
|
+
const items = inferSchemaFromValue(value[0]);
|
|
34
|
+
return items ? { type: "array", items } : void 0;
|
|
35
|
+
}
|
|
36
|
+
if (typeof value === "object") {
|
|
37
|
+
const properties = {};
|
|
38
|
+
const required = [];
|
|
39
|
+
for (const [key, childValue] of Object.entries(
|
|
40
|
+
value
|
|
41
|
+
)) {
|
|
42
|
+
const childSchema = inferSchemaFromValue(childValue);
|
|
43
|
+
if (!childSchema) {
|
|
44
|
+
return void 0;
|
|
45
|
+
}
|
|
46
|
+
properties[key] = childSchema;
|
|
47
|
+
if (childValue !== void 0) {
|
|
48
|
+
required.push(key);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
type: "object",
|
|
53
|
+
properties,
|
|
54
|
+
required,
|
|
55
|
+
additionalProperties: false
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return void 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/create-form-tools.ts
|
|
62
|
+
function formatFormErrors(errors, message) {
|
|
63
|
+
const lines = errors.map(
|
|
64
|
+
(error) => `${error.path ? `${error.path}: ` : ""}${error.message || error.kind}`
|
|
65
|
+
);
|
|
66
|
+
return [message, ...lines].filter(Boolean).join("\n");
|
|
67
|
+
}
|
|
68
|
+
function applyRedactResult(result, redactResult) {
|
|
69
|
+
if (!redactResult) {
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
return redactResult(result);
|
|
73
|
+
}
|
|
74
|
+
function createFormValidationSummaryTool(options) {
|
|
75
|
+
const schema = options.schema ?? inferSchemaFromValue(options.getValues());
|
|
76
|
+
return {
|
|
77
|
+
name: `${options.name}_validation_summary`,
|
|
78
|
+
description: "Returns form schema, validation messages, and submitting/validating state without submitting.",
|
|
79
|
+
inputSchema: {
|
|
80
|
+
type: "object",
|
|
81
|
+
properties: {},
|
|
82
|
+
additionalProperties: false
|
|
83
|
+
},
|
|
84
|
+
annotations: { readOnlyHint: true },
|
|
85
|
+
execute: () => {
|
|
86
|
+
const summary = {
|
|
87
|
+
name: options.name,
|
|
88
|
+
errors: options.getErrors?.() ?? [],
|
|
89
|
+
schema,
|
|
90
|
+
...options.getSummary?.()
|
|
91
|
+
};
|
|
92
|
+
if (options.redactValues) {
|
|
93
|
+
return {
|
|
94
|
+
...summary,
|
|
95
|
+
values: options.redactValues(options.getValues())
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return summary;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function createFormSubmitTool(options) {
|
|
103
|
+
const schema = options.schema ?? inferSchemaFromValue(options.getValues());
|
|
104
|
+
if (!schema) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`Could not infer WebMCP schema for form "${options.name}". Provide an explicit schema or use concrete non-null defaults.`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
name: options.name,
|
|
111
|
+
description: options.description,
|
|
112
|
+
inputSchema: schema,
|
|
113
|
+
annotations: {
|
|
114
|
+
readOnlyHint: false,
|
|
115
|
+
...options.annotations
|
|
116
|
+
},
|
|
117
|
+
validateArgs: options.validateArgs,
|
|
118
|
+
execute: async (rawArgs) => {
|
|
119
|
+
const values = options.validateArgs ? options.validateArgs(rawArgs) : rawArgs;
|
|
120
|
+
await options.setValues(values);
|
|
121
|
+
const result = await options.submit();
|
|
122
|
+
if (result.success) {
|
|
123
|
+
let output = result.message ?? "Form submitted successfully.";
|
|
124
|
+
if (result.data !== void 0) {
|
|
125
|
+
output = {
|
|
126
|
+
message: output,
|
|
127
|
+
data: options.redactValues ? options.redactValues(result.data) : result.data
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
return applyRedactResult(output, options.redactResult);
|
|
131
|
+
}
|
|
132
|
+
const errors = result.errors ?? options.getErrors?.() ?? [];
|
|
133
|
+
return applyRedactResult(
|
|
134
|
+
{
|
|
135
|
+
content: [
|
|
136
|
+
{
|
|
137
|
+
type: "text",
|
|
138
|
+
text: `Form submission failed:
|
|
139
|
+
${formatFormErrors(
|
|
140
|
+
errors,
|
|
141
|
+
result.message
|
|
142
|
+
)}`
|
|
143
|
+
}
|
|
144
|
+
],
|
|
145
|
+
isError: true
|
|
146
|
+
},
|
|
147
|
+
options.redactResult
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function createWebMcpFormTools(options) {
|
|
153
|
+
const includeValidationSummary = options.includeValidationSummary ?? true;
|
|
154
|
+
const submitTool = createFormSubmitTool(options);
|
|
155
|
+
if (includeValidationSummary) {
|
|
156
|
+
return [
|
|
157
|
+
submitTool,
|
|
158
|
+
createFormValidationSummaryTool(options)
|
|
159
|
+
];
|
|
160
|
+
}
|
|
161
|
+
return [submitTool];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// src/useWebMcpFormTool.ts
|
|
165
|
+
function useWebMcpFormTool(options, deps = []) {
|
|
166
|
+
const schema = useMemo(
|
|
167
|
+
() => options.schema ?? inferSchemaFromValue(options.getValues()),
|
|
168
|
+
[options]
|
|
169
|
+
);
|
|
170
|
+
if (!schema) {
|
|
171
|
+
throw new Error(
|
|
172
|
+
`Could not infer WebMCP schema for form "${options.name}". Provide an explicit schema or use concrete non-null defaults.`
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
const tools = useMemo(
|
|
176
|
+
() => createWebMcpFormTools(options),
|
|
177
|
+
[options]
|
|
178
|
+
);
|
|
179
|
+
useWebMcpTools(tools, deps, { source: "form" });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// src/formik.ts
|
|
183
|
+
function flattenErrors(errors, prefix = "") {
|
|
184
|
+
if (!errors || typeof errors !== "object") {
|
|
185
|
+
return [];
|
|
186
|
+
}
|
|
187
|
+
return Object.entries(errors).flatMap(
|
|
188
|
+
([key, value]) => {
|
|
189
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
190
|
+
if (typeof value === "string") {
|
|
191
|
+
return [{ path, message: value }];
|
|
192
|
+
}
|
|
193
|
+
return flattenErrors(value, path);
|
|
194
|
+
}
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
function useFormikWebMcpTool({
|
|
198
|
+
formik,
|
|
199
|
+
...options
|
|
200
|
+
}) {
|
|
201
|
+
useWebMcpFormTool({
|
|
202
|
+
...options,
|
|
203
|
+
getValues: () => formik.values,
|
|
204
|
+
setValues: (values) => formik.setValues(values),
|
|
205
|
+
getErrors: () => flattenErrors(formik.errors),
|
|
206
|
+
getSummary: () => ({
|
|
207
|
+
submitting: formik.isSubmitting,
|
|
208
|
+
validating: formik.isValidating,
|
|
209
|
+
touched: Boolean(formik.touched)
|
|
210
|
+
}),
|
|
211
|
+
submit: async () => {
|
|
212
|
+
const errors = await formik.validateForm();
|
|
213
|
+
const flattened = flattenErrors(errors);
|
|
214
|
+
if (flattened.length > 0) {
|
|
215
|
+
return { success: false, errors: flattened };
|
|
216
|
+
}
|
|
217
|
+
await formik.submitForm();
|
|
218
|
+
return { success: true };
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// src/native-form.ts
|
|
224
|
+
function formDataToObject(form) {
|
|
225
|
+
return Object.fromEntries(new FormData(form).entries());
|
|
226
|
+
}
|
|
227
|
+
function useNativeFormWebMcpTool(options) {
|
|
228
|
+
if (!options.schema) {
|
|
229
|
+
throw new Error("Native form WebMCP tools require an explicit schema.");
|
|
230
|
+
}
|
|
231
|
+
useWebMcpFormTool({
|
|
232
|
+
...options,
|
|
233
|
+
getValues: () => {
|
|
234
|
+
const form = options.formRef.current;
|
|
235
|
+
return form ? formDataToObject(form) : {};
|
|
236
|
+
},
|
|
237
|
+
setValues: (values) => {
|
|
238
|
+
const form = options.formRef.current;
|
|
239
|
+
if (!form) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
for (const [name, value] of Object.entries(values)) {
|
|
243
|
+
const field = form.elements.namedItem(name);
|
|
244
|
+
if (field && "value" in field) {
|
|
245
|
+
field.value = String(value ?? "");
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
getErrors: () => [],
|
|
250
|
+
submit: () => {
|
|
251
|
+
const form = options.formRef.current;
|
|
252
|
+
if (!form) {
|
|
253
|
+
return { success: false, message: "Form is not mounted." };
|
|
254
|
+
}
|
|
255
|
+
if (!form.checkValidity()) {
|
|
256
|
+
form.reportValidity();
|
|
257
|
+
return { success: false, message: "Native form validation failed." };
|
|
258
|
+
}
|
|
259
|
+
form.requestSubmit();
|
|
260
|
+
return { success: true, message: "Native form submitted." };
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// src/react-hook-form.ts
|
|
266
|
+
function flattenReactHookFormErrors(errors, prefix = "") {
|
|
267
|
+
if (!errors || typeof errors !== "object") {
|
|
268
|
+
return [];
|
|
269
|
+
}
|
|
270
|
+
return Object.entries(errors).flatMap(
|
|
271
|
+
([key, value]) => {
|
|
272
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
273
|
+
if (value?.message || value?.type) {
|
|
274
|
+
return [
|
|
275
|
+
{
|
|
276
|
+
path,
|
|
277
|
+
message: String(value.message ?? value.type),
|
|
278
|
+
kind: value.type ? String(value.type) : void 0
|
|
279
|
+
}
|
|
280
|
+
];
|
|
281
|
+
}
|
|
282
|
+
return flattenReactHookFormErrors(value, path);
|
|
283
|
+
}
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
function useReactHookFormWebMcpTool({
|
|
287
|
+
form,
|
|
288
|
+
onValidSubmit,
|
|
289
|
+
...options
|
|
290
|
+
}) {
|
|
291
|
+
useWebMcpFormTool({
|
|
292
|
+
...options,
|
|
293
|
+
getValues: () => form.getValues(),
|
|
294
|
+
setValues: async (values) => {
|
|
295
|
+
form.reset(values);
|
|
296
|
+
await form.trigger();
|
|
297
|
+
},
|
|
298
|
+
getErrors: () => flattenReactHookFormErrors(form.formState.errors),
|
|
299
|
+
getSummary: () => ({
|
|
300
|
+
submitting: form.formState.isSubmitting,
|
|
301
|
+
validating: form.formState.isValidating,
|
|
302
|
+
dirty: form.formState.isDirty,
|
|
303
|
+
touched: Boolean(form.formState.touchedFields)
|
|
304
|
+
}),
|
|
305
|
+
submit: async () => {
|
|
306
|
+
let valid = false;
|
|
307
|
+
let data;
|
|
308
|
+
await form.handleSubmit(
|
|
309
|
+
async (values) => {
|
|
310
|
+
valid = true;
|
|
311
|
+
data = await onValidSubmit(values);
|
|
312
|
+
},
|
|
313
|
+
async () => {
|
|
314
|
+
valid = false;
|
|
315
|
+
}
|
|
316
|
+
)();
|
|
317
|
+
if (valid) {
|
|
318
|
+
return { success: true, data };
|
|
319
|
+
}
|
|
320
|
+
return {
|
|
321
|
+
success: false,
|
|
322
|
+
errors: flattenReactHookFormErrors(form.formState.errors)
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// src/tanstack-form.ts
|
|
329
|
+
function flattenTanStackErrors(errors = []) {
|
|
330
|
+
return errors.map((error, index) => ({
|
|
331
|
+
path: typeof error === "object" && error && "path" in error ? String(error.path) : String(index),
|
|
332
|
+
message: typeof error === "object" && error && "message" in error ? String(error.message) : String(error)
|
|
333
|
+
}));
|
|
334
|
+
}
|
|
335
|
+
function useTanStackFormWebMcpTool({
|
|
336
|
+
form,
|
|
337
|
+
...options
|
|
338
|
+
}) {
|
|
339
|
+
useWebMcpFormTool({
|
|
340
|
+
...options,
|
|
341
|
+
getValues: () => form.state.values,
|
|
342
|
+
setValues: (values) => {
|
|
343
|
+
for (const [field, value] of Object.entries(
|
|
344
|
+
values
|
|
345
|
+
)) {
|
|
346
|
+
form.setFieldValue?.(field, value);
|
|
347
|
+
}
|
|
348
|
+
},
|
|
349
|
+
getErrors: () => flattenTanStackErrors(form.state.errors),
|
|
350
|
+
getSummary: () => ({
|
|
351
|
+
submitting: form.state.isSubmitting,
|
|
352
|
+
validating: form.state.isValidating,
|
|
353
|
+
dirty: form.state.isDirty
|
|
354
|
+
}),
|
|
355
|
+
submit: async () => {
|
|
356
|
+
await form.handleSubmit();
|
|
357
|
+
const errors = flattenTanStackErrors(form.state.errors);
|
|
358
|
+
return errors.length > 0 ? { success: false, errors } : { success: true };
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
export {
|
|
363
|
+
FormValidationError,
|
|
364
|
+
createFormSubmitTool,
|
|
365
|
+
createFormValidationSummaryTool,
|
|
366
|
+
createWebMcpFormTools,
|
|
367
|
+
flattenReactHookFormErrors,
|
|
368
|
+
formDataToObject,
|
|
369
|
+
inferSchemaFromValue,
|
|
370
|
+
useFormikWebMcpTool,
|
|
371
|
+
useNativeFormWebMcpTool,
|
|
372
|
+
useReactHookFormWebMcpTool,
|
|
373
|
+
useTanStackFormWebMcpTool,
|
|
374
|
+
useWebMcpFormTool
|
|
375
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"infer-schema.d.ts","sourceRoot":"","sources":["../src/infer-schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpD,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,UAAU,GAAG,SAAS,CAqD3E"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export function inferSchemaFromValue(value) {
|
|
2
|
+
if (typeof value === 'string') {
|
|
3
|
+
return { type: 'string' };
|
|
4
|
+
}
|
|
5
|
+
if (typeof value === 'number') {
|
|
6
|
+
return { type: 'number' };
|
|
7
|
+
}
|
|
8
|
+
if (typeof value === 'boolean') {
|
|
9
|
+
return { type: 'boolean' };
|
|
10
|
+
}
|
|
11
|
+
if (value === null || value === undefined) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
if (Array.isArray(value)) {
|
|
15
|
+
if (value.length === 0) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
const items = inferSchemaFromValue(value[0]);
|
|
19
|
+
return items ? { type: 'array', items } : undefined;
|
|
20
|
+
}
|
|
21
|
+
if (typeof value === 'object') {
|
|
22
|
+
const properties = {};
|
|
23
|
+
const required = [];
|
|
24
|
+
for (const [key, childValue] of Object.entries(value)) {
|
|
25
|
+
const childSchema = inferSchemaFromValue(childValue);
|
|
26
|
+
if (!childSchema) {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
properties[key] = childSchema;
|
|
30
|
+
if (childValue !== undefined) {
|
|
31
|
+
required.push(key);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
type: 'object',
|
|
36
|
+
properties,
|
|
37
|
+
required,
|
|
38
|
+
additionalProperties: false,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"infer-schema.test.d.ts","sourceRoot":"","sources":["../src/infer-schema.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { inferSchemaFromValue } from './infer-schema';
|
|
3
|
+
describe('inferSchemaFromValue', () => {
|
|
4
|
+
it('infers primitive types', () => {
|
|
5
|
+
expect(inferSchemaFromValue('hello')).toEqual({ type: 'string' });
|
|
6
|
+
expect(inferSchemaFromValue(42)).toEqual({ type: 'number' });
|
|
7
|
+
expect(inferSchemaFromValue(3.14)).toEqual({ type: 'number' });
|
|
8
|
+
expect(inferSchemaFromValue(true)).toEqual({ type: 'boolean' });
|
|
9
|
+
});
|
|
10
|
+
it('returns undefined for null, undefined, and empty arrays', () => {
|
|
11
|
+
expect(inferSchemaFromValue(null)).toBeUndefined();
|
|
12
|
+
expect(inferSchemaFromValue(undefined)).toBeUndefined();
|
|
13
|
+
expect(inferSchemaFromValue([])).toBeUndefined();
|
|
14
|
+
});
|
|
15
|
+
it('infers nested object schemas', () => {
|
|
16
|
+
expect(inferSchemaFromValue({ name: 'Ada', age: 30, active: true })).toEqual({
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: {
|
|
19
|
+
name: { type: 'string' },
|
|
20
|
+
age: { type: 'number' },
|
|
21
|
+
active: { type: 'boolean' },
|
|
22
|
+
},
|
|
23
|
+
required: ['name', 'age', 'active'],
|
|
24
|
+
additionalProperties: false,
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { RefObject } from 'react';
|
|
2
|
+
import type { WebMcpFormToolOptions } from './types';
|
|
3
|
+
export interface NativeFormWebMcpOptions extends Pick<WebMcpFormToolOptions<Record<string, unknown>>, 'name' | 'description' | 'schema' | 'validateArgs' | 'annotations'> {
|
|
4
|
+
formRef: RefObject<HTMLFormElement | null>;
|
|
5
|
+
}
|
|
6
|
+
export declare function formDataToObject(form: HTMLFormElement): Record<string, unknown>;
|
|
7
|
+
export declare function useNativeFormWebMcpTool(options: NativeFormWebMcpOptions): void;
|
|
8
|
+
//# sourceMappingURL=native-form.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"native-form.d.ts","sourceRoot":"","sources":["../src/native-form.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAErD,MAAM,WAAW,uBACf,SAAQ,IAAI,CACV,qBAAqB,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EAC9C,MAAM,GAAG,aAAa,GAAG,QAAQ,GAAG,cAAc,GAAG,aAAa,CACnE;IACD,OAAO,EAAE,SAAS,CAAC,eAAe,GAAG,IAAI,CAAC,CAAC;CAC5C;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAE/E;AAED,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,uBAAuB,GAC/B,IAAI,CAwCN"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { useWebMcpFormTool } from './useWebMcpFormTool';
|
|
2
|
+
export function formDataToObject(form) {
|
|
3
|
+
return Object.fromEntries(new FormData(form).entries());
|
|
4
|
+
}
|
|
5
|
+
export function useNativeFormWebMcpTool(options) {
|
|
6
|
+
if (!options.schema) {
|
|
7
|
+
throw new Error('Native form WebMCP tools require an explicit schema.');
|
|
8
|
+
}
|
|
9
|
+
useWebMcpFormTool({
|
|
10
|
+
...options,
|
|
11
|
+
getValues: () => {
|
|
12
|
+
const form = options.formRef.current;
|
|
13
|
+
return form ? formDataToObject(form) : {};
|
|
14
|
+
},
|
|
15
|
+
setValues: (values) => {
|
|
16
|
+
const form = options.formRef.current;
|
|
17
|
+
if (!form) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
for (const [name, value] of Object.entries(values)) {
|
|
21
|
+
const field = form.elements.namedItem(name);
|
|
22
|
+
if (field && 'value' in field) {
|
|
23
|
+
field.value = String(value ?? '');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
getErrors: () => [],
|
|
28
|
+
submit: () => {
|
|
29
|
+
const form = options.formRef.current;
|
|
30
|
+
if (!form) {
|
|
31
|
+
return { success: false, message: 'Form is not mounted.' };
|
|
32
|
+
}
|
|
33
|
+
if (!form.checkValidity()) {
|
|
34
|
+
form.reportValidity();
|
|
35
|
+
return { success: false, message: 'Native form validation failed.' };
|
|
36
|
+
}
|
|
37
|
+
form.requestSubmit();
|
|
38
|
+
return { success: true, message: 'Native form submitted.' };
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { FormError, WebMcpFormToolOptions } from './types';
|
|
2
|
+
export interface ReactHookFormLike<TValues> {
|
|
3
|
+
getValues(): TValues;
|
|
4
|
+
reset(values: TValues): void;
|
|
5
|
+
trigger(): Promise<boolean>;
|
|
6
|
+
handleSubmit(onValid: (values: TValues) => void | Promise<void>, onInvalid?: () => void | Promise<void>): () => Promise<void> | void;
|
|
7
|
+
formState: {
|
|
8
|
+
errors?: unknown;
|
|
9
|
+
isSubmitting?: boolean;
|
|
10
|
+
isValidating?: boolean;
|
|
11
|
+
isDirty?: boolean;
|
|
12
|
+
touchedFields?: unknown;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export interface ReactHookFormWebMcpOptions<TValues> extends Pick<WebMcpFormToolOptions<TValues>, 'name' | 'description' | 'schema' | 'validateArgs' | 'annotations'> {
|
|
16
|
+
form: ReactHookFormLike<TValues>;
|
|
17
|
+
onValidSubmit: (values: TValues) => unknown | Promise<unknown>;
|
|
18
|
+
}
|
|
19
|
+
export declare function flattenReactHookFormErrors(errors: unknown, prefix?: string): FormError[];
|
|
20
|
+
export declare function useReactHookFormWebMcpTool<TValues>({ form, onValidSubmit, ...options }: ReactHookFormWebMcpOptions<TValues>): void;
|
|
21
|
+
//# sourceMappingURL=react-hook-form.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"react-hook-form.d.ts","sourceRoot":"","sources":["../src/react-hook-form.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAEhE,MAAM,WAAW,iBAAiB,CAAC,OAAO;IACxC,SAAS,IAAI,OAAO,CAAC;IACrB,KAAK,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC;IAC7B,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5B,YAAY,CACV,OAAO,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,EAClD,SAAS,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GACrC,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC9B,SAAS,EAAE;QACT,MAAM,CAAC,EAAE,OAAO,CAAC;QACjB,YAAY,CAAC,EAAE,OAAO,CAAC;QACvB,YAAY,CAAC,EAAE,OAAO,CAAC;QACvB,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,aAAa,CAAC,EAAE,OAAO,CAAC;KACzB,CAAC;CACH;AAED,MAAM,WAAW,0BAA0B,CAAC,OAAO,CACjD,SAAQ,IAAI,CACV,qBAAqB,CAAC,OAAO,CAAC,EAC9B,MAAM,GAAG,aAAa,GAAG,QAAQ,GAAG,cAAc,GAAG,aAAa,CACnE;IACD,IAAI,EAAE,iBAAiB,CAAC,OAAO,CAAC,CAAC;IACjC,aAAa,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAChE;AAED,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,OAAO,EACf,MAAM,SAAK,GACV,SAAS,EAAE,CAqBb;AAED,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,EAClD,IAAI,EACJ,aAAa,EACb,GAAG,OAAO,EACX,EAAE,0BAA0B,CAAC,OAAO,CAAC,GAAG,IAAI,CAuC5C"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { useWebMcpFormTool } from './useWebMcpFormTool';
|
|
2
|
+
export function flattenReactHookFormErrors(errors, prefix = '') {
|
|
3
|
+
if (!errors || typeof errors !== 'object') {
|
|
4
|
+
return [];
|
|
5
|
+
}
|
|
6
|
+
return Object.entries(errors).flatMap(([key, value]) => {
|
|
7
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
8
|
+
if (value?.message || value?.type) {
|
|
9
|
+
return [
|
|
10
|
+
{
|
|
11
|
+
path,
|
|
12
|
+
message: String(value.message ?? value.type),
|
|
13
|
+
kind: value.type ? String(value.type) : undefined,
|
|
14
|
+
},
|
|
15
|
+
];
|
|
16
|
+
}
|
|
17
|
+
return flattenReactHookFormErrors(value, path);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
export function useReactHookFormWebMcpTool({ form, onValidSubmit, ...options }) {
|
|
21
|
+
useWebMcpFormTool({
|
|
22
|
+
...options,
|
|
23
|
+
getValues: () => form.getValues(),
|
|
24
|
+
setValues: async (values) => {
|
|
25
|
+
form.reset(values);
|
|
26
|
+
await form.trigger();
|
|
27
|
+
},
|
|
28
|
+
getErrors: () => flattenReactHookFormErrors(form.formState.errors),
|
|
29
|
+
getSummary: () => ({
|
|
30
|
+
submitting: form.formState.isSubmitting,
|
|
31
|
+
validating: form.formState.isValidating,
|
|
32
|
+
dirty: form.formState.isDirty,
|
|
33
|
+
touched: Boolean(form.formState.touchedFields),
|
|
34
|
+
}),
|
|
35
|
+
submit: async () => {
|
|
36
|
+
let valid = false;
|
|
37
|
+
let data;
|
|
38
|
+
await form.handleSubmit(async (values) => {
|
|
39
|
+
valid = true;
|
|
40
|
+
data = await onValidSubmit(values);
|
|
41
|
+
}, async () => {
|
|
42
|
+
valid = false;
|
|
43
|
+
})();
|
|
44
|
+
if (valid) {
|
|
45
|
+
return { success: true, data };
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
success: false,
|
|
49
|
+
errors: flattenReactHookFormErrors(form.formState.errors),
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"react-hook-form.test.d.ts","sourceRoot":"","sources":["../src/react-hook-form.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { flattenReactHookFormErrors } from './react-hook-form';
|
|
3
|
+
describe('flattenReactHookFormErrors', () => {
|
|
4
|
+
it('flattens nested react-hook-form errors', () => {
|
|
5
|
+
const errors = flattenReactHookFormErrors({
|
|
6
|
+
email: { type: 'required', message: 'Email is required' },
|
|
7
|
+
profile: {
|
|
8
|
+
name: { type: 'min', message: 'Name is too short' },
|
|
9
|
+
},
|
|
10
|
+
});
|
|
11
|
+
expect(errors).toEqual([
|
|
12
|
+
{
|
|
13
|
+
path: 'email',
|
|
14
|
+
message: 'Email is required',
|
|
15
|
+
kind: 'required',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
path: 'profile.name',
|
|
19
|
+
message: 'Name is too short',
|
|
20
|
+
kind: 'min',
|
|
21
|
+
},
|
|
22
|
+
]);
|
|
23
|
+
});
|
|
24
|
+
it('returns an empty array for non-object errors', () => {
|
|
25
|
+
expect(flattenReactHookFormErrors(null)).toEqual([]);
|
|
26
|
+
expect(flattenReactHookFormErrors(undefined)).toEqual([]);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { WebMcpFormToolOptions } from './types';
|
|
2
|
+
export interface TanStackFormLike<TValues> {
|
|
3
|
+
state: {
|
|
4
|
+
values: TValues;
|
|
5
|
+
errors?: unknown[];
|
|
6
|
+
isSubmitting?: boolean;
|
|
7
|
+
isValidating?: boolean;
|
|
8
|
+
isDirty?: boolean;
|
|
9
|
+
};
|
|
10
|
+
setFieldValue?: (field: string, value: unknown) => void;
|
|
11
|
+
handleSubmit: () => void | Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
export interface TanStackFormWebMcpOptions<TValues> extends Pick<WebMcpFormToolOptions<TValues>, 'name' | 'description' | 'schema' | 'validateArgs' | 'annotations'> {
|
|
14
|
+
form: TanStackFormLike<TValues>;
|
|
15
|
+
}
|
|
16
|
+
export declare function useTanStackFormWebMcpTool<TValues>({ form, ...options }: TanStackFormWebMcpOptions<TValues>): void;
|
|
17
|
+
//# sourceMappingURL=tanstack-form.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tanstack-form.d.ts","sourceRoot":"","sources":["../src/tanstack-form.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAa,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAEhE,MAAM,WAAW,gBAAgB,CAAC,OAAO;IACvC,KAAK,EAAE;QACL,MAAM,EAAE,OAAO,CAAC;QAChB,MAAM,CAAC,EAAE,OAAO,EAAE,CAAC;QACnB,YAAY,CAAC,EAAE,OAAO,CAAC;QACvB,YAAY,CAAC,EAAE,OAAO,CAAC;QACvB,OAAO,CAAC,EAAE,OAAO,CAAC;KACnB,CAAC;IACF,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IACxD,YAAY,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1C;AAED,MAAM,WAAW,yBAAyB,CAAC,OAAO,CAChD,SAAQ,IAAI,CACV,qBAAqB,CAAC,OAAO,CAAC,EAC9B,MAAM,GAAG,aAAa,GAAG,QAAQ,GAAG,cAAc,GAAG,aAAa,CACnE;IACD,IAAI,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAC;CACjC;AAeD,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,EACjD,IAAI,EACJ,GAAG,OAAO,EACX,EAAE,yBAAyB,CAAC,OAAO,CAAC,GAAG,IAAI,CAuB3C"}
|