react-webmcp 0.1.1 → 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
 
@@ -23,10 +23,12 @@ function isWebMCPTestingAvailable() {
23
23
  return typeof window !== "undefined" && typeof window.navigator !== "undefined" && !!window.navigator.modelContextTesting;
24
24
  }
25
25
  function warnIfUnavailable(hookName) {
26
- if (!isWebMCPAvailable()) {
27
- console.warn(
28
- `[react-webmcp] ${hookName}: navigator.modelContext is not available. Ensure you are running Chrome 146+ with the "WebMCP for testing" flag enabled.`
29
- );
26
+ if (process.env.NODE_ENV !== "production") {
27
+ if (!isWebMCPAvailable()) {
28
+ console.warn(
29
+ `[react-webmcp] ${hookName}: navigator.modelContext is not available. Ensure you are running Chrome 146+ with the "WebMCP for testing" flag enabled.`
30
+ );
31
+ }
30
32
  }
31
33
  }
32
34
 
@@ -35,11 +37,11 @@ function toolFingerprint(config) {
35
37
  return `${config.name}::${config.description}::${JSON.stringify(config.inputSchema)}::${JSON.stringify(config.outputSchema ?? {})}::${JSON.stringify(config.annotations ?? {})}`;
36
38
  }
37
39
  function useWebMCPTool(config) {
38
- const registeredNameRef = React2.useRef(null);
39
- const configRef = React2.useRef(config);
40
+ const registeredNameRef = React6.useRef(null);
41
+ const configRef = React6.useRef(config);
40
42
  configRef.current = config;
41
43
  const fingerprint = toolFingerprint(config);
42
- React2.useEffect(() => {
44
+ React6.useEffect(() => {
43
45
  const mc = getModelContext();
44
46
  if (!mc) {
45
47
  warnIfUnavailable("useWebMCPTool");
@@ -55,17 +57,26 @@ function useWebMCPTool(config) {
55
57
  name: config.name,
56
58
  description: config.description,
57
59
  inputSchema: config.inputSchema,
58
- ...config.outputSchema ? { outputSchema: config.outputSchema } : {},
59
- ...config.annotations ? { annotations: config.annotations } : {},
60
60
  execute: (input) => {
61
61
  return configRef.current.execute(input);
62
62
  }
63
63
  };
64
+ if (config.outputSchema) {
65
+ toolDef.outputSchema = config.outputSchema;
66
+ }
67
+ if (config.annotations) {
68
+ toolDef.annotations = config.annotations;
69
+ }
64
70
  try {
65
71
  mc.registerTool(toolDef);
66
72
  registeredNameRef.current = config.name;
67
73
  } catch (err) {
68
- console.error(`[react-webmcp] Failed to register tool "${config.name}":`, err);
74
+ if (process.env.NODE_ENV !== "production") {
75
+ console.error(
76
+ `[react-webmcp] Failed to register tool "${config.name}":`,
77
+ err
78
+ );
79
+ }
69
80
  }
70
81
  return () => {
71
82
  try {
@@ -82,25 +93,40 @@ function toolsFingerprint(tools) {
82
93
  ).join("|");
83
94
  }
84
95
  function useWebMCPContext(config) {
85
- const toolsRef = React2.useRef(config.tools);
96
+ const toolsRef = React6.useRef(config.tools);
86
97
  toolsRef.current = config.tools;
87
98
  const fingerprint = toolsFingerprint(config.tools);
88
- React2.useEffect(() => {
99
+ React6.useEffect(() => {
89
100
  const mc = getModelContext();
90
101
  if (!mc) {
91
102
  warnIfUnavailable("useWebMCPContext");
92
103
  return;
93
104
  }
94
- const stableTools = toolsRef.current.map((tool, idx) => ({
95
- ...tool,
96
- execute: (input) => {
97
- return toolsRef.current[idx].execute(input);
105
+ const stableTools = toolsRef.current.map((tool, idx) => {
106
+ const def = {
107
+ name: tool.name,
108
+ description: tool.description,
109
+ inputSchema: tool.inputSchema,
110
+ execute: (input) => {
111
+ return toolsRef.current[idx].execute(input);
112
+ }
113
+ };
114
+ if (tool.annotations) {
115
+ def.annotations = tool.annotations;
116
+ }
117
+ if (tool.outputSchema) {
118
+ def.outputSchema = tool.outputSchema;
98
119
  }
99
- }));
120
+ return def;
121
+ });
100
122
  try {
101
- mc.provideContext({ tools: stableTools });
123
+ mc.provideContext({
124
+ tools: stableTools
125
+ });
102
126
  } catch (err) {
103
- console.error("[react-webmcp] Failed to provide context:", err);
127
+ if (process.env.NODE_ENV !== "production") {
128
+ console.error("[react-webmcp] Failed to provide context:", err);
129
+ }
104
130
  }
105
131
  return () => {
106
132
  try {
@@ -111,7 +137,7 @@ function useWebMCPContext(config) {
111
137
  }, [fingerprint]);
112
138
  }
113
139
  function useToolEvent(event, callback, toolNameFilter) {
114
- React2.useEffect(() => {
140
+ React6.useEffect(() => {
115
141
  const handler = (e) => {
116
142
  const toolName = e.toolName ?? e.detail?.toolName;
117
143
  if (!toolName) return;
@@ -134,8 +160,8 @@ function WebMCPForm({
134
160
  children,
135
161
  ...rest
136
162
  }) {
137
- const formRef = React2.useRef(null);
138
- React2.useEffect(() => {
163
+ const formRef = React6.useRef(null);
164
+ React6.useEffect(() => {
139
165
  const handleActivated = (e) => {
140
166
  const name = e.toolName ?? e.detail?.toolName;
141
167
  if (name === toolName && onToolActivated) {
@@ -155,7 +181,7 @@ function WebMCPForm({
155
181
  window.removeEventListener("toolcancel", handleCancel);
156
182
  };
157
183
  }, [toolName, onToolActivated, onToolCancel]);
158
- const handleSubmit = React2.useCallback(
184
+ const handleSubmit = React6.useCallback(
159
185
  (e) => {
160
186
  if (onSubmit) {
161
187
  onSubmit(e.nativeEvent);
@@ -181,7 +207,7 @@ function WebMCPForm({
181
207
  }
182
208
  );
183
209
  }
184
- var WebMCPInput = React2__default.default.forwardRef(
210
+ var WebMCPInput = React6__default.default.forwardRef(
185
211
  ({ toolParamTitle, toolParamDescription, ...rest }, ref) => {
186
212
  const webmcpAttrs = {};
187
213
  if (toolParamTitle) {
@@ -194,7 +220,7 @@ var WebMCPInput = React2__default.default.forwardRef(
194
220
  }
195
221
  );
196
222
  WebMCPInput.displayName = "WebMCPInput";
197
- var WebMCPSelect = React2__default.default.forwardRef(({ toolParamTitle, toolParamDescription, children, ...rest }, ref) => {
223
+ var WebMCPSelect = React6__default.default.forwardRef(({ toolParamTitle, toolParamDescription, children, ...rest }, ref) => {
198
224
  const webmcpAttrs = {};
199
225
  if (toolParamTitle) {
200
226
  webmcpAttrs.toolparamtitle = toolParamTitle;
@@ -205,7 +231,7 @@ var WebMCPSelect = React2__default.default.forwardRef(({ toolParamTitle, toolPar
205
231
  return /* @__PURE__ */ jsxRuntime.jsx("select", { ref, ...webmcpAttrs, ...rest, children });
206
232
  });
207
233
  WebMCPSelect.displayName = "WebMCPSelect";
208
- var WebMCPTextarea = React2__default.default.forwardRef(({ toolParamTitle, toolParamDescription, ...rest }, ref) => {
234
+ var WebMCPTextarea = React6__default.default.forwardRef(({ toolParamTitle, toolParamDescription, ...rest }, ref) => {
209
235
  const webmcpAttrs = {};
210
236
  if (toolParamTitle) {
211
237
  webmcpAttrs.toolparamtitle = toolParamTitle;
@@ -216,12 +242,12 @@ var WebMCPTextarea = React2__default.default.forwardRef(({ toolParamTitle, toolP
216
242
  return /* @__PURE__ */ jsxRuntime.jsx("textarea", { ref, ...webmcpAttrs, ...rest });
217
243
  });
218
244
  WebMCPTextarea.displayName = "WebMCPTextarea";
219
- var WebMCPReactContext = React2.createContext({
245
+ var WebMCPReactContext = React6.createContext({
220
246
  available: false,
221
247
  testingAvailable: false
222
248
  });
223
249
  function WebMCPProvider({ children }) {
224
- const value = React2.useMemo(
250
+ const value = React6.useMemo(
225
251
  () => ({
226
252
  available: isWebMCPAvailable(),
227
253
  testingAvailable: isWebMCPTestingAvailable()
@@ -231,20 +257,354 @@ function WebMCPProvider({ children }) {
231
257
  return /* @__PURE__ */ jsxRuntime.jsx(WebMCPReactContext.Provider, { value, children });
232
258
  }
233
259
  function useWebMCPStatus() {
234
- return React2.useContext(WebMCPReactContext);
260
+ return React6.useContext(WebMCPReactContext);
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("|");
235
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 };
236
587
 
588
+ exports.WebMCP = WebMCP;
589
+ exports.WebMCPField = WebMCPField;
237
590
  exports.WebMCPForm = WebMCPForm;
238
591
  exports.WebMCPInput = WebMCPInput;
239
592
  exports.WebMCPProvider = WebMCPProvider;
240
593
  exports.WebMCPSelect = WebMCPSelect;
241
594
  exports.WebMCPTextarea = WebMCPTextarea;
595
+ exports.WebMCPTool = WebMCPTool;
596
+ exports.buildInputSchema = buildInputSchema;
597
+ exports.extractFields = extractFields;
598
+ exports.extractOptions = extractOptions;
242
599
  exports.getModelContext = getModelContext;
243
600
  exports.isWebMCPAvailable = isWebMCPAvailable;
244
601
  exports.isWebMCPTestingAvailable = isWebMCPTestingAvailable;
602
+ exports.useRegisterField = useRegisterField;
603
+ exports.useSchemaCollector = useSchemaCollector;
245
604
  exports.useToolEvent = useToolEvent;
246
605
  exports.useWebMCPContext = useWebMCPContext;
247
606
  exports.useWebMCPStatus = useWebMCPStatus;
248
607
  exports.useWebMCPTool = useWebMCPTool;
608
+ exports.validateSchema = validateSchema;
249
609
  //# sourceMappingURL=index.js.map
250
610
  //# sourceMappingURL=index.js.map