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.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import React2, { createContext, useRef, useEffect, useCallback, useMemo, useContext } from 'react';
2
- import { jsx } from 'react/jsx-runtime';
1
+ import React6, { createContext, useRef, useEffect, useCallback, useMemo, useContext, useState } from 'react';
2
+ import { jsx, Fragment } from 'react/jsx-runtime';
3
3
 
4
4
  // src/hooks/useWebMCPTool.ts
5
5
 
@@ -17,10 +17,12 @@ function isWebMCPTestingAvailable() {
17
17
  return typeof window !== "undefined" && typeof window.navigator !== "undefined" && !!window.navigator.modelContextTesting;
18
18
  }
19
19
  function warnIfUnavailable(hookName) {
20
- if (!isWebMCPAvailable()) {
21
- console.warn(
22
- `[react-webmcp] ${hookName}: navigator.modelContext is not available. Ensure you are running Chrome 146+ with the "WebMCP for testing" flag enabled.`
23
- );
20
+ if (process.env.NODE_ENV !== "production") {
21
+ if (!isWebMCPAvailable()) {
22
+ console.warn(
23
+ `[react-webmcp] ${hookName}: navigator.modelContext is not available. Ensure you are running Chrome 146+ with the "WebMCP for testing" flag enabled.`
24
+ );
25
+ }
24
26
  }
25
27
  }
26
28
 
@@ -49,17 +51,26 @@ function useWebMCPTool(config) {
49
51
  name: config.name,
50
52
  description: config.description,
51
53
  inputSchema: config.inputSchema,
52
- ...config.outputSchema ? { outputSchema: config.outputSchema } : {},
53
- ...config.annotations ? { annotations: config.annotations } : {},
54
54
  execute: (input) => {
55
55
  return configRef.current.execute(input);
56
56
  }
57
57
  };
58
+ if (config.outputSchema) {
59
+ toolDef.outputSchema = config.outputSchema;
60
+ }
61
+ if (config.annotations) {
62
+ toolDef.annotations = config.annotations;
63
+ }
58
64
  try {
59
65
  mc.registerTool(toolDef);
60
66
  registeredNameRef.current = config.name;
61
67
  } catch (err) {
62
- console.error(`[react-webmcp] Failed to register tool "${config.name}":`, err);
68
+ if (process.env.NODE_ENV !== "production") {
69
+ console.error(
70
+ `[react-webmcp] Failed to register tool "${config.name}":`,
71
+ err
72
+ );
73
+ }
63
74
  }
64
75
  return () => {
65
76
  try {
@@ -85,16 +96,31 @@ function useWebMCPContext(config) {
85
96
  warnIfUnavailable("useWebMCPContext");
86
97
  return;
87
98
  }
88
- const stableTools = toolsRef.current.map((tool, idx) => ({
89
- ...tool,
90
- execute: (input) => {
91
- return toolsRef.current[idx].execute(input);
99
+ const stableTools = toolsRef.current.map((tool, idx) => {
100
+ const def = {
101
+ name: tool.name,
102
+ description: tool.description,
103
+ inputSchema: tool.inputSchema,
104
+ execute: (input) => {
105
+ return toolsRef.current[idx].execute(input);
106
+ }
107
+ };
108
+ if (tool.annotations) {
109
+ def.annotations = tool.annotations;
110
+ }
111
+ if (tool.outputSchema) {
112
+ def.outputSchema = tool.outputSchema;
92
113
  }
93
- }));
114
+ return def;
115
+ });
94
116
  try {
95
- mc.provideContext({ tools: stableTools });
117
+ mc.provideContext({
118
+ tools: stableTools
119
+ });
96
120
  } catch (err) {
97
- console.error("[react-webmcp] Failed to provide context:", err);
121
+ if (process.env.NODE_ENV !== "production") {
122
+ console.error("[react-webmcp] Failed to provide context:", err);
123
+ }
98
124
  }
99
125
  return () => {
100
126
  try {
@@ -175,7 +201,7 @@ function WebMCPForm({
175
201
  }
176
202
  );
177
203
  }
178
- var WebMCPInput = React2.forwardRef(
204
+ var WebMCPInput = React6.forwardRef(
179
205
  ({ toolParamTitle, toolParamDescription, ...rest }, ref) => {
180
206
  const webmcpAttrs = {};
181
207
  if (toolParamTitle) {
@@ -188,7 +214,7 @@ var WebMCPInput = React2.forwardRef(
188
214
  }
189
215
  );
190
216
  WebMCPInput.displayName = "WebMCPInput";
191
- var WebMCPSelect = React2.forwardRef(({ toolParamTitle, toolParamDescription, children, ...rest }, ref) => {
217
+ var WebMCPSelect = React6.forwardRef(({ toolParamTitle, toolParamDescription, children, ...rest }, ref) => {
192
218
  const webmcpAttrs = {};
193
219
  if (toolParamTitle) {
194
220
  webmcpAttrs.toolparamtitle = toolParamTitle;
@@ -199,7 +225,7 @@ var WebMCPSelect = React2.forwardRef(({ toolParamTitle, toolParamDescription, ch
199
225
  return /* @__PURE__ */ jsx("select", { ref, ...webmcpAttrs, ...rest, children });
200
226
  });
201
227
  WebMCPSelect.displayName = "WebMCPSelect";
202
- var WebMCPTextarea = React2.forwardRef(({ toolParamTitle, toolParamDescription, ...rest }, ref) => {
228
+ var WebMCPTextarea = React6.forwardRef(({ toolParamTitle, toolParamDescription, ...rest }, ref) => {
203
229
  const webmcpAttrs = {};
204
230
  if (toolParamTitle) {
205
231
  webmcpAttrs.toolparamtitle = toolParamTitle;
@@ -227,7 +253,332 @@ function WebMCPProvider({ children }) {
227
253
  function useWebMCPStatus() {
228
254
  return useContext(WebMCPReactContext);
229
255
  }
256
+ function extractOptions(children) {
257
+ const results = [];
258
+ React6.Children.toArray(children).forEach((child) => {
259
+ if (!React6.isValidElement(child)) return;
260
+ const props = child.props;
261
+ if (props.value !== void 0 && props.value !== null) {
262
+ const value = props.value;
263
+ let label;
264
+ if (typeof props.children === "string") {
265
+ label = props.children;
266
+ } else {
267
+ label = String(value);
268
+ }
269
+ results.push({ value, label });
270
+ }
271
+ if (props.children) {
272
+ results.push(...extractOptions(props.children));
273
+ }
274
+ });
275
+ return results;
276
+ }
277
+ function extractFields(children) {
278
+ const fields = [];
279
+ React6.Children.toArray(children).forEach((child) => {
280
+ if (!React6.isValidElement(child)) return;
281
+ const props = child.props;
282
+ const inputProps = props.inputProps;
283
+ const slotInput = props.slotProps?.input;
284
+ const name = props.name ?? inputProps?.name ?? slotInput?.name;
285
+ if (name) {
286
+ const field = { name };
287
+ if (props.type !== void 0) field.type = props.type;
288
+ if (props.required !== void 0) field.required = Boolean(props.required);
289
+ if (props.min !== void 0) field.min = Number(props.min);
290
+ if (props.max !== void 0) field.max = Number(props.max);
291
+ if (props.minLength !== void 0) field.minLength = Number(props.minLength);
292
+ if (props.maxLength !== void 0) field.maxLength = Number(props.maxLength);
293
+ if (props.pattern !== void 0) field.pattern = props.pattern;
294
+ if (props.children) {
295
+ const options = extractOptions(props.children);
296
+ if (options.length > 0) {
297
+ field.enumValues = options.map((o) => o.value);
298
+ field.oneOf = options;
299
+ }
300
+ }
301
+ fields.push(field);
302
+ } else if (props.children) {
303
+ fields.push(...extractFields(props.children));
304
+ }
305
+ });
306
+ return fields;
307
+ }
308
+
309
+ // src/adapters/buildSchema.ts
310
+ function mapHtmlTypeToSchemaType(htmlType) {
311
+ switch (htmlType) {
312
+ case "number":
313
+ case "range":
314
+ return "number";
315
+ case "checkbox":
316
+ return "boolean";
317
+ default:
318
+ return "string";
319
+ }
320
+ }
321
+ function buildInputSchema(fields) {
322
+ const properties = {};
323
+ const required = [];
324
+ const sortedFields = [...fields].sort((a, b) => a.name.localeCompare(b.name));
325
+ for (const field of sortedFields) {
326
+ const prop = {
327
+ type: mapHtmlTypeToSchemaType(field.type)
328
+ };
329
+ if (field.title) prop.title = field.title;
330
+ if (field.description) prop.description = field.description;
331
+ if (field.min !== void 0) prop.minimum = field.min;
332
+ if (field.max !== void 0) prop.maximum = field.max;
333
+ if (field.minLength !== void 0) prop.minLength = field.minLength;
334
+ if (field.maxLength !== void 0) prop.maxLength = field.maxLength;
335
+ if (field.pattern) prop.pattern = field.pattern;
336
+ if (field.enumValues && field.enumValues.length > 0) {
337
+ prop.enum = field.enumValues;
338
+ }
339
+ if (field.oneOf && field.oneOf.length > 0) {
340
+ prop.oneOf = field.oneOf.map((opt) => ({
341
+ const: opt.value,
342
+ title: opt.label
343
+ }));
344
+ }
345
+ properties[field.name] = prop;
346
+ if (field.required) {
347
+ required.push(field.name);
348
+ }
349
+ }
350
+ const schema = {
351
+ type: "object",
352
+ properties
353
+ };
354
+ if (required.length > 0) {
355
+ schema.required = required.sort();
356
+ }
357
+ return schema;
358
+ }
359
+
360
+ // src/adapters/validateSchema.ts
361
+ function validateSchema(fields, options) {
362
+ if (process.env.NODE_ENV === "production") return;
363
+ const strict = options?.strict ?? false;
364
+ const issues = [];
365
+ const seen = /* @__PURE__ */ new Set();
366
+ for (const field of fields) {
367
+ if (seen.has(field.name)) {
368
+ issues.push(`Duplicate field name "${field.name}".`);
369
+ }
370
+ seen.add(field.name);
371
+ const schemaType = mapHtmlTypeToSchemaType(field.type);
372
+ if (field.pattern !== void 0 && schemaType !== "string") {
373
+ issues.push(
374
+ `Field "${field.name}": pattern is only valid for string types, but type is "${schemaType}".`
375
+ );
376
+ }
377
+ if ((field.min !== void 0 || field.max !== void 0) && schemaType !== "number") {
378
+ issues.push(
379
+ `Field "${field.name}": min/max are only valid for number types, but type is "${schemaType}".`
380
+ );
381
+ }
382
+ if ((field.minLength !== void 0 || field.maxLength !== void 0) && schemaType !== "string") {
383
+ issues.push(
384
+ `Field "${field.name}": minLength/maxLength are only valid for string types, but type is "${schemaType}".`
385
+ );
386
+ }
387
+ if (field.enumValues && field.enumValues.length > 0) {
388
+ for (const val of field.enumValues) {
389
+ const valType = typeof val;
390
+ if (schemaType === "string" && valType !== "string") {
391
+ issues.push(
392
+ `Field "${field.name}": enum value ${JSON.stringify(val)} is not a string.`
393
+ );
394
+ }
395
+ if (schemaType === "number" && valType !== "number") {
396
+ issues.push(
397
+ `Field "${field.name}": enum value ${JSON.stringify(val)} is not a number.`
398
+ );
399
+ }
400
+ if (schemaType === "boolean" && valType !== "boolean") {
401
+ issues.push(
402
+ `Field "${field.name}": enum value ${JSON.stringify(val)} is not a boolean.`
403
+ );
404
+ }
405
+ }
406
+ }
407
+ }
408
+ for (const issue of issues) {
409
+ if (strict) {
410
+ throw new Error(`[react-webmcp] ${issue}`);
411
+ }
412
+ console.warn(`[react-webmcp] ${issue}`);
413
+ }
414
+ }
415
+
416
+ // src/adapters/useSchemaCollector.ts
417
+ var ToolContext = createContext(null);
418
+ function fieldsFingerprint(fields) {
419
+ return fields.map(
420
+ (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 ?? ""}`
421
+ ).join("|");
422
+ }
423
+ function mergeField(base, override) {
424
+ const result = { ...base };
425
+ for (const key of Object.keys(override)) {
426
+ if (override[key] !== void 0) {
427
+ result[key] = override[key];
428
+ }
429
+ }
430
+ return result;
431
+ }
432
+ function useSchemaCollector({
433
+ children,
434
+ fields: fieldsProp,
435
+ strict
436
+ }) {
437
+ const contextFieldsRef = useRef(/* @__PURE__ */ new Map());
438
+ const [version, setVersion] = useState(0);
439
+ const registerField = useCallback((field) => {
440
+ contextFieldsRef.current.set(field.name, field);
441
+ setVersion((v) => v + 1);
442
+ }, []);
443
+ const unregisterField = useCallback((name) => {
444
+ contextFieldsRef.current.delete(name);
445
+ setVersion((v) => v + 1);
446
+ }, []);
447
+ const childrenFields = extractFields(children);
448
+ const childrenFP = fieldsFingerprint(childrenFields);
449
+ const merged = useMemo(() => {
450
+ const fieldMap = /* @__PURE__ */ new Map();
451
+ for (const field of childrenFields) {
452
+ fieldMap.set(field.name, field);
453
+ }
454
+ if (fieldsProp) {
455
+ for (const [name, overrides] of Object.entries(fieldsProp)) {
456
+ const existing = fieldMap.get(name);
457
+ if (existing) {
458
+ fieldMap.set(name, mergeField(existing, overrides));
459
+ } else {
460
+ fieldMap.set(name, { name, ...overrides });
461
+ }
462
+ }
463
+ }
464
+ for (const [name, field] of contextFieldsRef.current) {
465
+ const existing = fieldMap.get(name);
466
+ if (existing) {
467
+ fieldMap.set(name, mergeField(existing, field));
468
+ } else {
469
+ fieldMap.set(name, field);
470
+ }
471
+ }
472
+ const result = Array.from(fieldMap.values());
473
+ if (process.env.NODE_ENV !== "production") {
474
+ validateSchema(result, { strict });
475
+ }
476
+ return result;
477
+ }, [childrenFP, fieldsProp, version, strict]);
478
+ const mergedFP = fieldsFingerprint(merged);
479
+ const schema = useMemo(
480
+ () => buildInputSchema(merged),
481
+ // eslint-disable-next-line react-hooks/exhaustive-deps
482
+ [mergedFP]
483
+ );
484
+ return { schema, registerField, unregisterField };
485
+ }
486
+ function WebMCPTool({
487
+ name,
488
+ description,
489
+ onExecute,
490
+ fields: fieldsProp,
491
+ strict,
492
+ autoSubmit,
493
+ annotations,
494
+ onToolActivated,
495
+ onToolCancel,
496
+ children
497
+ }) {
498
+ const { schema, registerField, unregisterField } = useSchemaCollector({
499
+ children,
500
+ fields: fieldsProp,
501
+ strict
502
+ });
503
+ const executeRef = useRef(onExecute);
504
+ executeRef.current = onExecute;
505
+ useWebMCPTool({
506
+ name,
507
+ description,
508
+ inputSchema: schema,
509
+ annotations,
510
+ execute: (input) => executeRef.current(input)
511
+ });
512
+ useEffect(() => {
513
+ if (typeof window === "undefined") return;
514
+ if (!onToolActivated && !onToolCancel) return;
515
+ const handleActivated = (e) => {
516
+ const toolName = e.toolName ?? e.detail?.toolName;
517
+ if (toolName === name && onToolActivated) {
518
+ onToolActivated(toolName);
519
+ }
520
+ };
521
+ const handleCancel = (e) => {
522
+ const toolName = e.toolName ?? e.detail?.toolName;
523
+ if (toolName === name && onToolCancel) {
524
+ onToolCancel(toolName);
525
+ }
526
+ };
527
+ window.addEventListener("toolactivated", handleActivated);
528
+ window.addEventListener("toolcancel", handleCancel);
529
+ return () => {
530
+ window.removeEventListener("toolactivated", handleActivated);
531
+ window.removeEventListener("toolcancel", handleCancel);
532
+ };
533
+ }, [name, onToolActivated, onToolCancel]);
534
+ return /* @__PURE__ */ jsx(ToolContext.Provider, { value: { registerField, unregisterField }, children });
535
+ }
536
+ WebMCPTool.displayName = "WebMCP.Tool";
537
+ function fieldFingerprint(field) {
538
+ 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 ?? ""}`;
539
+ }
540
+ function useRegisterField(field) {
541
+ const ctx = useContext(ToolContext);
542
+ const fp = fieldFingerprint(field);
543
+ useEffect(() => {
544
+ if (!ctx) {
545
+ if (process.env.NODE_ENV !== "production") {
546
+ console.warn(
547
+ `[react-webmcp] useRegisterField: no WebMCP.Tool context found for field "${field.name}". Wrap this component in a <WebMCP.Tool> to register fields.`
548
+ );
549
+ }
550
+ return;
551
+ }
552
+ ctx.registerField(field);
553
+ return () => {
554
+ try {
555
+ ctx.unregisterField(field.name);
556
+ } catch {
557
+ }
558
+ };
559
+ }, [fp]);
560
+ }
561
+ function WebMCPField({
562
+ children,
563
+ name,
564
+ ...rest
565
+ }) {
566
+ const field = { name, ...rest };
567
+ if (!field.enumValues && !field.oneOf) {
568
+ const options = extractOptions(children);
569
+ if (options.length > 0) {
570
+ field.enumValues = options.map((o) => o.value);
571
+ field.oneOf = options;
572
+ }
573
+ }
574
+ useRegisterField(field);
575
+ return /* @__PURE__ */ jsx(Fragment, { children });
576
+ }
577
+ WebMCPField.displayName = "WebMCP.Field";
578
+
579
+ // src/adapters/index.ts
580
+ var WebMCP = { Tool: WebMCPTool, Field: WebMCPField };
230
581
 
231
- export { WebMCPForm, WebMCPInput, WebMCPProvider, WebMCPSelect, WebMCPTextarea, getModelContext, isWebMCPAvailable, isWebMCPTestingAvailable, useToolEvent, useWebMCPContext, useWebMCPStatus, useWebMCPTool };
582
+ export { WebMCP, WebMCPField, WebMCPForm, WebMCPInput, WebMCPProvider, WebMCPSelect, WebMCPTextarea, WebMCPTool, buildInputSchema, extractFields, extractOptions, getModelContext, isWebMCPAvailable, isWebMCPTestingAvailable, useRegisterField, useSchemaCollector, useToolEvent, useWebMCPContext, useWebMCPStatus, useWebMCPTool, validateSchema };
232
583
  //# sourceMappingURL=index.mjs.map
233
584
  //# sourceMappingURL=index.mjs.map