react-webmcp 0.2.0 → 0.2.1

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/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  'use strict';
2
2
 
3
- var React2 = require('react');
3
+ var React6 = require('react');
4
4
  var jsxRuntime = require('react/jsx-runtime');
5
5
 
6
6
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
7
7
 
8
- var React2__default = /*#__PURE__*/_interopDefault(React2);
8
+ var React6__default = /*#__PURE__*/_interopDefault(React6);
9
9
 
10
10
  // src/hooks/useWebMCPTool.ts
11
11
 
@@ -37,11 +37,11 @@ function toolFingerprint(config) {
37
37
  return `${config.name}::${config.description}::${JSON.stringify(config.inputSchema)}::${JSON.stringify(config.outputSchema ?? {})}::${JSON.stringify(config.annotations ?? {})}`;
38
38
  }
39
39
  function useWebMCPTool(config) {
40
- const registeredNameRef = React2.useRef(null);
41
- const configRef = React2.useRef(config);
40
+ const registeredNameRef = React6.useRef(null);
41
+ const configRef = React6.useRef(config);
42
42
  configRef.current = config;
43
43
  const fingerprint = toolFingerprint(config);
44
- React2.useEffect(() => {
44
+ React6.useEffect(() => {
45
45
  const mc = getModelContext();
46
46
  if (!mc) {
47
47
  warnIfUnavailable("useWebMCPTool");
@@ -93,10 +93,10 @@ function toolsFingerprint(tools) {
93
93
  ).join("|");
94
94
  }
95
95
  function useWebMCPContext(config) {
96
- const toolsRef = React2.useRef(config.tools);
96
+ const toolsRef = React6.useRef(config.tools);
97
97
  toolsRef.current = config.tools;
98
98
  const fingerprint = toolsFingerprint(config.tools);
99
- React2.useEffect(() => {
99
+ React6.useEffect(() => {
100
100
  const mc = getModelContext();
101
101
  if (!mc) {
102
102
  warnIfUnavailable("useWebMCPContext");
@@ -137,7 +137,7 @@ function useWebMCPContext(config) {
137
137
  }, [fingerprint]);
138
138
  }
139
139
  function useToolEvent(event, callback, toolNameFilter) {
140
- React2.useEffect(() => {
140
+ React6.useEffect(() => {
141
141
  const handler = (e) => {
142
142
  const toolName = e.toolName ?? e.detail?.toolName;
143
143
  if (!toolName) return;
@@ -160,8 +160,8 @@ function WebMCPForm({
160
160
  children,
161
161
  ...rest
162
162
  }) {
163
- const formRef = React2.useRef(null);
164
- React2.useEffect(() => {
163
+ const formRef = React6.useRef(null);
164
+ React6.useEffect(() => {
165
165
  const handleActivated = (e) => {
166
166
  const name = e.toolName ?? e.detail?.toolName;
167
167
  if (name === toolName && onToolActivated) {
@@ -181,7 +181,7 @@ function WebMCPForm({
181
181
  window.removeEventListener("toolcancel", handleCancel);
182
182
  };
183
183
  }, [toolName, onToolActivated, onToolCancel]);
184
- const handleSubmit = React2.useCallback(
184
+ const handleSubmit = React6.useCallback(
185
185
  (e) => {
186
186
  if (onSubmit) {
187
187
  onSubmit(e.nativeEvent);
@@ -207,7 +207,7 @@ function WebMCPForm({
207
207
  }
208
208
  );
209
209
  }
210
- var WebMCPInput = React2__default.default.forwardRef(
210
+ var WebMCPInput = React6__default.default.forwardRef(
211
211
  ({ toolParamTitle, toolParamDescription, ...rest }, ref) => {
212
212
  const webmcpAttrs = {};
213
213
  if (toolParamTitle) {
@@ -220,7 +220,7 @@ var WebMCPInput = React2__default.default.forwardRef(
220
220
  }
221
221
  );
222
222
  WebMCPInput.displayName = "WebMCPInput";
223
- var WebMCPSelect = React2__default.default.forwardRef(({ toolParamTitle, toolParamDescription, children, ...rest }, ref) => {
223
+ var WebMCPSelect = React6__default.default.forwardRef(({ toolParamTitle, toolParamDescription, children, ...rest }, ref) => {
224
224
  const webmcpAttrs = {};
225
225
  if (toolParamTitle) {
226
226
  webmcpAttrs.toolparamtitle = toolParamTitle;
@@ -231,7 +231,7 @@ var WebMCPSelect = React2__default.default.forwardRef(({ toolParamTitle, toolPar
231
231
  return /* @__PURE__ */ jsxRuntime.jsx("select", { ref, ...webmcpAttrs, ...rest, children });
232
232
  });
233
233
  WebMCPSelect.displayName = "WebMCPSelect";
234
- var WebMCPTextarea = React2__default.default.forwardRef(({ toolParamTitle, toolParamDescription, ...rest }, ref) => {
234
+ var WebMCPTextarea = React6__default.default.forwardRef(({ toolParamTitle, toolParamDescription, ...rest }, ref) => {
235
235
  const webmcpAttrs = {};
236
236
  if (toolParamTitle) {
237
237
  webmcpAttrs.toolparamtitle = toolParamTitle;
@@ -242,12 +242,12 @@ var WebMCPTextarea = React2__default.default.forwardRef(({ toolParamTitle, toolP
242
242
  return /* @__PURE__ */ jsxRuntime.jsx("textarea", { ref, ...webmcpAttrs, ...rest });
243
243
  });
244
244
  WebMCPTextarea.displayName = "WebMCPTextarea";
245
- var WebMCPReactContext = React2.createContext({
245
+ var WebMCPReactContext = React6.createContext({
246
246
  available: false,
247
247
  testingAvailable: false
248
248
  });
249
249
  function WebMCPProvider({ children }) {
250
- const value = React2.useMemo(
250
+ const value = React6.useMemo(
251
251
  () => ({
252
252
  available: isWebMCPAvailable(),
253
253
  testingAvailable: isWebMCPTestingAvailable()
@@ -257,20 +257,354 @@ function WebMCPProvider({ children }) {
257
257
  return /* @__PURE__ */ jsxRuntime.jsx(WebMCPReactContext.Provider, { value, children });
258
258
  }
259
259
  function useWebMCPStatus() {
260
- return React2.useContext(WebMCPReactContext);
260
+ return React6.useContext(WebMCPReactContext);
261
261
  }
262
+ function extractOptions(children) {
263
+ const results = [];
264
+ React6__default.default.Children.toArray(children).forEach((child) => {
265
+ if (!React6__default.default.isValidElement(child)) return;
266
+ const props = child.props;
267
+ if (props.value !== void 0 && props.value !== null) {
268
+ const value = props.value;
269
+ let label;
270
+ if (typeof props.children === "string") {
271
+ label = props.children;
272
+ } else {
273
+ label = String(value);
274
+ }
275
+ results.push({ value, label });
276
+ }
277
+ if (props.children) {
278
+ results.push(...extractOptions(props.children));
279
+ }
280
+ });
281
+ return results;
282
+ }
283
+ function extractFields(children) {
284
+ const fields = [];
285
+ React6__default.default.Children.toArray(children).forEach((child) => {
286
+ if (!React6__default.default.isValidElement(child)) return;
287
+ const props = child.props;
288
+ const inputProps = props.inputProps;
289
+ const slotInput = props.slotProps?.input;
290
+ const name = props.name ?? inputProps?.name ?? slotInput?.name;
291
+ if (name) {
292
+ const field = { name };
293
+ if (props.type !== void 0) field.type = props.type;
294
+ if (props.required !== void 0) field.required = Boolean(props.required);
295
+ if (props.min !== void 0) field.min = Number(props.min);
296
+ if (props.max !== void 0) field.max = Number(props.max);
297
+ if (props.minLength !== void 0) field.minLength = Number(props.minLength);
298
+ if (props.maxLength !== void 0) field.maxLength = Number(props.maxLength);
299
+ if (props.pattern !== void 0) field.pattern = props.pattern;
300
+ if (props.children) {
301
+ const options = extractOptions(props.children);
302
+ if (options.length > 0) {
303
+ field.enumValues = options.map((o) => o.value);
304
+ field.oneOf = options;
305
+ }
306
+ }
307
+ fields.push(field);
308
+ } else if (props.children) {
309
+ fields.push(...extractFields(props.children));
310
+ }
311
+ });
312
+ return fields;
313
+ }
314
+
315
+ // src/adapters/buildSchema.ts
316
+ function mapHtmlTypeToSchemaType(htmlType) {
317
+ switch (htmlType) {
318
+ case "number":
319
+ case "range":
320
+ return "number";
321
+ case "checkbox":
322
+ return "boolean";
323
+ default:
324
+ return "string";
325
+ }
326
+ }
327
+ function buildInputSchema(fields) {
328
+ const properties = {};
329
+ const required = [];
330
+ const sortedFields = [...fields].sort((a, b) => a.name.localeCompare(b.name));
331
+ for (const field of sortedFields) {
332
+ const prop = {
333
+ type: mapHtmlTypeToSchemaType(field.type)
334
+ };
335
+ if (field.title) prop.title = field.title;
336
+ if (field.description) prop.description = field.description;
337
+ if (field.min !== void 0) prop.minimum = field.min;
338
+ if (field.max !== void 0) prop.maximum = field.max;
339
+ if (field.minLength !== void 0) prop.minLength = field.minLength;
340
+ if (field.maxLength !== void 0) prop.maxLength = field.maxLength;
341
+ if (field.pattern) prop.pattern = field.pattern;
342
+ if (field.enumValues && field.enumValues.length > 0) {
343
+ prop.enum = field.enumValues;
344
+ }
345
+ if (field.oneOf && field.oneOf.length > 0) {
346
+ prop.oneOf = field.oneOf.map((opt) => ({
347
+ const: opt.value,
348
+ title: opt.label
349
+ }));
350
+ }
351
+ properties[field.name] = prop;
352
+ if (field.required) {
353
+ required.push(field.name);
354
+ }
355
+ }
356
+ const schema = {
357
+ type: "object",
358
+ properties
359
+ };
360
+ if (required.length > 0) {
361
+ schema.required = required.sort();
362
+ }
363
+ return schema;
364
+ }
365
+
366
+ // src/adapters/validateSchema.ts
367
+ function validateSchema(fields, options) {
368
+ if (process.env.NODE_ENV === "production") return;
369
+ const strict = options?.strict ?? false;
370
+ const issues = [];
371
+ const seen = /* @__PURE__ */ new Set();
372
+ for (const field of fields) {
373
+ if (seen.has(field.name)) {
374
+ issues.push(`Duplicate field name "${field.name}".`);
375
+ }
376
+ seen.add(field.name);
377
+ const schemaType = mapHtmlTypeToSchemaType(field.type);
378
+ if (field.pattern !== void 0 && schemaType !== "string") {
379
+ issues.push(
380
+ `Field "${field.name}": pattern is only valid for string types, but type is "${schemaType}".`
381
+ );
382
+ }
383
+ if ((field.min !== void 0 || field.max !== void 0) && schemaType !== "number") {
384
+ issues.push(
385
+ `Field "${field.name}": min/max are only valid for number types, but type is "${schemaType}".`
386
+ );
387
+ }
388
+ if ((field.minLength !== void 0 || field.maxLength !== void 0) && schemaType !== "string") {
389
+ issues.push(
390
+ `Field "${field.name}": minLength/maxLength are only valid for string types, but type is "${schemaType}".`
391
+ );
392
+ }
393
+ if (field.enumValues && field.enumValues.length > 0) {
394
+ for (const val of field.enumValues) {
395
+ const valType = typeof val;
396
+ if (schemaType === "string" && valType !== "string") {
397
+ issues.push(
398
+ `Field "${field.name}": enum value ${JSON.stringify(val)} is not a string.`
399
+ );
400
+ }
401
+ if (schemaType === "number" && valType !== "number") {
402
+ issues.push(
403
+ `Field "${field.name}": enum value ${JSON.stringify(val)} is not a number.`
404
+ );
405
+ }
406
+ if (schemaType === "boolean" && valType !== "boolean") {
407
+ issues.push(
408
+ `Field "${field.name}": enum value ${JSON.stringify(val)} is not a boolean.`
409
+ );
410
+ }
411
+ }
412
+ }
413
+ }
414
+ for (const issue of issues) {
415
+ if (strict) {
416
+ throw new Error(`[react-webmcp] ${issue}`);
417
+ }
418
+ console.warn(`[react-webmcp] ${issue}`);
419
+ }
420
+ }
421
+
422
+ // src/adapters/useSchemaCollector.ts
423
+ var ToolContext = React6.createContext(null);
424
+ function fieldsFingerprint(fields) {
425
+ return fields.map(
426
+ (f) => `${f.name}::${f.type ?? ""}::${f.required ?? ""}::${f.title ?? ""}::${f.description ?? ""}::${JSON.stringify(f.enumValues ?? [])}::${JSON.stringify(f.oneOf ?? [])}::${f.min ?? ""}::${f.max ?? ""}::${f.minLength ?? ""}::${f.maxLength ?? ""}::${f.pattern ?? ""}`
427
+ ).join("|");
428
+ }
429
+ function mergeField(base, override) {
430
+ const result = { ...base };
431
+ for (const key of Object.keys(override)) {
432
+ if (override[key] !== void 0) {
433
+ result[key] = override[key];
434
+ }
435
+ }
436
+ return result;
437
+ }
438
+ function useSchemaCollector({
439
+ children,
440
+ fields: fieldsProp,
441
+ strict
442
+ }) {
443
+ const contextFieldsRef = React6.useRef(/* @__PURE__ */ new Map());
444
+ const [version, setVersion] = React6.useState(0);
445
+ const registerField = React6.useCallback((field) => {
446
+ contextFieldsRef.current.set(field.name, field);
447
+ setVersion((v) => v + 1);
448
+ }, []);
449
+ const unregisterField = React6.useCallback((name) => {
450
+ contextFieldsRef.current.delete(name);
451
+ setVersion((v) => v + 1);
452
+ }, []);
453
+ const childrenFields = extractFields(children);
454
+ const childrenFP = fieldsFingerprint(childrenFields);
455
+ const merged = React6.useMemo(() => {
456
+ const fieldMap = /* @__PURE__ */ new Map();
457
+ for (const field of childrenFields) {
458
+ fieldMap.set(field.name, field);
459
+ }
460
+ if (fieldsProp) {
461
+ for (const [name, overrides] of Object.entries(fieldsProp)) {
462
+ const existing = fieldMap.get(name);
463
+ if (existing) {
464
+ fieldMap.set(name, mergeField(existing, overrides));
465
+ } else {
466
+ fieldMap.set(name, { name, ...overrides });
467
+ }
468
+ }
469
+ }
470
+ for (const [name, field] of contextFieldsRef.current) {
471
+ const existing = fieldMap.get(name);
472
+ if (existing) {
473
+ fieldMap.set(name, mergeField(existing, field));
474
+ } else {
475
+ fieldMap.set(name, field);
476
+ }
477
+ }
478
+ const result = Array.from(fieldMap.values());
479
+ if (process.env.NODE_ENV !== "production") {
480
+ validateSchema(result, { strict });
481
+ }
482
+ return result;
483
+ }, [childrenFP, fieldsProp, version, strict]);
484
+ const mergedFP = fieldsFingerprint(merged);
485
+ const schema = React6.useMemo(
486
+ () => buildInputSchema(merged),
487
+ // eslint-disable-next-line react-hooks/exhaustive-deps
488
+ [mergedFP]
489
+ );
490
+ return { schema, registerField, unregisterField };
491
+ }
492
+ function WebMCPTool({
493
+ name,
494
+ description,
495
+ onExecute,
496
+ fields: fieldsProp,
497
+ strict,
498
+ autoSubmit,
499
+ annotations,
500
+ onToolActivated,
501
+ onToolCancel,
502
+ children
503
+ }) {
504
+ const { schema, registerField, unregisterField } = useSchemaCollector({
505
+ children,
506
+ fields: fieldsProp,
507
+ strict
508
+ });
509
+ const executeRef = React6.useRef(onExecute);
510
+ executeRef.current = onExecute;
511
+ useWebMCPTool({
512
+ name,
513
+ description,
514
+ inputSchema: schema,
515
+ annotations,
516
+ execute: (input) => executeRef.current(input)
517
+ });
518
+ React6.useEffect(() => {
519
+ if (typeof window === "undefined") return;
520
+ if (!onToolActivated && !onToolCancel) return;
521
+ const handleActivated = (e) => {
522
+ const toolName = e.toolName ?? e.detail?.toolName;
523
+ if (toolName === name && onToolActivated) {
524
+ onToolActivated(toolName);
525
+ }
526
+ };
527
+ const handleCancel = (e) => {
528
+ const toolName = e.toolName ?? e.detail?.toolName;
529
+ if (toolName === name && onToolCancel) {
530
+ onToolCancel(toolName);
531
+ }
532
+ };
533
+ window.addEventListener("toolactivated", handleActivated);
534
+ window.addEventListener("toolcancel", handleCancel);
535
+ return () => {
536
+ window.removeEventListener("toolactivated", handleActivated);
537
+ window.removeEventListener("toolcancel", handleCancel);
538
+ };
539
+ }, [name, onToolActivated, onToolCancel]);
540
+ return /* @__PURE__ */ jsxRuntime.jsx(ToolContext.Provider, { value: { registerField, unregisterField }, children });
541
+ }
542
+ WebMCPTool.displayName = "WebMCP.Tool";
543
+ function fieldFingerprint(field) {
544
+ return `${field.name}::${field.type ?? ""}::${field.required ?? ""}::${field.title ?? ""}::${field.description ?? ""}::${JSON.stringify(field.enumValues ?? [])}::${JSON.stringify(field.oneOf ?? [])}::${field.min ?? ""}::${field.max ?? ""}::${field.minLength ?? ""}::${field.maxLength ?? ""}::${field.pattern ?? ""}`;
545
+ }
546
+ function useRegisterField(field) {
547
+ const ctx = React6.useContext(ToolContext);
548
+ const fp = fieldFingerprint(field);
549
+ React6.useEffect(() => {
550
+ if (!ctx) {
551
+ if (process.env.NODE_ENV !== "production") {
552
+ console.warn(
553
+ `[react-webmcp] useRegisterField: no WebMCP.Tool context found for field "${field.name}". Wrap this component in a <WebMCP.Tool> to register fields.`
554
+ );
555
+ }
556
+ return;
557
+ }
558
+ ctx.registerField(field);
559
+ return () => {
560
+ try {
561
+ ctx.unregisterField(field.name);
562
+ } catch {
563
+ }
564
+ };
565
+ }, [fp]);
566
+ }
567
+ function WebMCPField({
568
+ children,
569
+ name,
570
+ ...rest
571
+ }) {
572
+ const field = { name, ...rest };
573
+ if (!field.enumValues && !field.oneOf) {
574
+ const options = extractOptions(children);
575
+ if (options.length > 0) {
576
+ field.enumValues = options.map((o) => o.value);
577
+ field.oneOf = options;
578
+ }
579
+ }
580
+ useRegisterField(field);
581
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children });
582
+ }
583
+ WebMCPField.displayName = "WebMCP.Field";
584
+
585
+ // src/adapters/index.ts
586
+ var WebMCP = { Tool: WebMCPTool, Field: WebMCPField };
262
587
 
588
+ exports.WebMCP = WebMCP;
589
+ exports.WebMCPField = WebMCPField;
263
590
  exports.WebMCPForm = WebMCPForm;
264
591
  exports.WebMCPInput = WebMCPInput;
265
592
  exports.WebMCPProvider = WebMCPProvider;
266
593
  exports.WebMCPSelect = WebMCPSelect;
267
594
  exports.WebMCPTextarea = WebMCPTextarea;
595
+ exports.WebMCPTool = WebMCPTool;
596
+ exports.buildInputSchema = buildInputSchema;
597
+ exports.extractFields = extractFields;
598
+ exports.extractOptions = extractOptions;
268
599
  exports.getModelContext = getModelContext;
269
600
  exports.isWebMCPAvailable = isWebMCPAvailable;
270
601
  exports.isWebMCPTestingAvailable = isWebMCPTestingAvailable;
602
+ exports.useRegisterField = useRegisterField;
603
+ exports.useSchemaCollector = useSchemaCollector;
271
604
  exports.useToolEvent = useToolEvent;
272
605
  exports.useWebMCPContext = useWebMCPContext;
273
606
  exports.useWebMCPStatus = useWebMCPStatus;
274
607
  exports.useWebMCPTool = useWebMCPTool;
608
+ exports.validateSchema = validateSchema;
275
609
  //# sourceMappingURL=index.js.map
276
610
  //# sourceMappingURL=index.js.map