simple-webmcp 0.1.0 → 0.3.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.
Files changed (49) hide show
  1. package/.agents/skills/webmcp-simple/SKILL.md +27 -12
  2. package/README.md +211 -71
  3. package/dist/dev-polyfill.cjs +68 -0
  4. package/dist/dev-polyfill.cjs.map +1 -0
  5. package/dist/dev-polyfill.d.cts +1 -0
  6. package/dist/dev-polyfill.d.ts +1 -0
  7. package/dist/dev-polyfill.js +62 -0
  8. package/dist/dev-polyfill.js.map +1 -0
  9. package/dist/devtools.cjs +452 -0
  10. package/dist/devtools.cjs.map +1 -0
  11. package/dist/devtools.d.cts +14 -0
  12. package/dist/devtools.d.ts +14 -0
  13. package/dist/devtools.js +449 -0
  14. package/dist/devtools.js.map +1 -0
  15. package/dist/index.cjs +350 -41
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.d.cts +60 -51
  18. package/dist/index.d.ts +60 -51
  19. package/dist/index.js +344 -42
  20. package/dist/index.js.map +1 -1
  21. package/dist/inspect.cjs +342 -0
  22. package/dist/inspect.cjs.map +1 -0
  23. package/dist/inspect.d.cts +37 -0
  24. package/dist/inspect.d.ts +37 -0
  25. package/dist/inspect.js +334 -0
  26. package/dist/inspect.js.map +1 -0
  27. package/dist/polyfill.cjs.map +1 -1
  28. package/dist/polyfill.d.cts +15 -7
  29. package/dist/polyfill.d.ts +15 -7
  30. package/dist/polyfill.js.map +1 -1
  31. package/dist/react.cjs +1191 -12
  32. package/dist/react.cjs.map +1 -1
  33. package/dist/react.d.cts +28 -10
  34. package/dist/react.d.ts +28 -10
  35. package/dist/react.js +1190 -15
  36. package/dist/react.js.map +1 -1
  37. package/dist/registry-A4DdpmDn.d.cts +38 -0
  38. package/dist/registry-Dscedahn.d.ts +38 -0
  39. package/dist/testing.cjs +68 -0
  40. package/dist/testing.cjs.map +1 -0
  41. package/dist/testing.d.cts +1 -0
  42. package/dist/testing.d.ts +1 -0
  43. package/dist/testing.js +62 -0
  44. package/dist/testing.js.map +1 -0
  45. package/dist/{types-D-cwSfEU.d.cts → types-CF8kTj5O.d.cts} +61 -2
  46. package/dist/{types-D-cwSfEU.d.ts → types-CF8kTj5O.d.ts} +61 -2
  47. package/dist/zod.d.cts +1 -1
  48. package/dist/zod.d.ts +1 -1
  49. package/package.json +30 -1
package/dist/index.cjs CHANGED
@@ -437,9 +437,15 @@ var Registry = class {
437
437
  constructor() {
438
438
  this.entries = /* @__PURE__ */ new Map();
439
439
  }
440
- // expose for devtools/tests
440
+ // expose for devtools/tests — now includes contract for inspect
441
441
  list() {
442
- return Array.from(this.entries.values()).map((e) => ({ name: e.name, status: e.status }));
442
+ return Array.from(this.entries.values()).map((e) => ({ name: e.name, status: e.status, contract: e.contract }));
443
+ }
444
+ get(name) {
445
+ return this.entries.get(name);
446
+ }
447
+ getContract(name) {
448
+ return this.entries.get(name)?.contract;
443
449
  }
444
450
  clear() {
445
451
  for (const e of this.entries.values()) {
@@ -463,37 +469,10 @@ var Registry = class {
463
469
  return existing.unregister;
464
470
  }
465
471
  if (!isWebMCPSupported()) {
466
- const controller2 = new AbortController();
467
- const entry2 = {
468
- name,
469
- controller: controller2,
470
- status: "registered",
471
- promise: Promise.resolve(),
472
- unregister: () => {
473
- try {
474
- controller2.abort();
475
- } catch {
476
- }
477
- this.entries.delete(name);
478
- }
472
+ if (opts?.signal?.aborted) return () => {
473
+ };
474
+ return () => {
479
475
  };
480
- if (opts?.signal) {
481
- if (opts.signal.aborted) {
482
- controller2.abort();
483
- this.entries.delete(name);
484
- return () => {
485
- };
486
- }
487
- opts.signal.addEventListener("abort", () => {
488
- try {
489
- controller2.abort();
490
- } catch {
491
- }
492
- this.entries.delete(name);
493
- }, { once: true });
494
- }
495
- this.entries.set(name, entry2);
496
- return entry2.unregister;
497
476
  }
498
477
  const modelContext = getModelContext();
499
478
  if (!modelContext) throw new NotSupportedError();
@@ -529,7 +508,9 @@ var Registry = class {
529
508
  entry.status = "unregistered";
530
509
  this.entries.delete(name);
531
510
  }
532
- }
511
+ },
512
+ contract,
513
+ execute: opts?.execute
533
514
  };
534
515
  this.entries.set(name, entry);
535
516
  const wrappedExecute = opts?.execute ?? (async (args) => ({ content: [{ type: "text", text: `no execute for ${name}` }] }));
@@ -578,7 +559,12 @@ var Registry = class {
578
559
  this.entries.delete(name);
579
560
  }
580
561
  };
581
- var registry = new Registry();
562
+ var REGISTRY_KEY = "__simpleWebmcpRegistry";
563
+ var globalAny = globalThis;
564
+ var registry = globalAny[REGISTRY_KEY] ?? new Registry();
565
+ if (!globalAny[REGISTRY_KEY]) {
566
+ globalAny[REGISTRY_KEY] = registry;
567
+ }
582
568
  function getRegistry() {
583
569
  return registry;
584
570
  }
@@ -608,7 +594,12 @@ function normalizeResult(value) {
608
594
  }
609
595
  }
610
596
  function normalizeError(err) {
611
- const message = err instanceof Error ? err.message : typeof err === "string" ? err : (() => {
597
+ const message = (() => {
598
+ if (err instanceof Error) return err.message;
599
+ if (typeof err === "string") return err;
600
+ if (err && typeof err === "object" && "message" in err && typeof err.message === "string") {
601
+ return err.message;
602
+ }
612
603
  try {
613
604
  return JSON.stringify(err);
614
605
  } catch {
@@ -638,6 +629,247 @@ function wrapExecute(fn, opts) {
638
629
  };
639
630
  }
640
631
 
632
+ // src/hooks/engine.ts
633
+ var fallbackCounter = 0;
634
+ function genInvocationId() {
635
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
636
+ return crypto.randomUUID();
637
+ }
638
+ return fallbackInvocationId();
639
+ }
640
+ function fallbackInvocationId() {
641
+ fallbackCounter = (fallbackCounter + 1) % Number.MAX_SAFE_INTEGER;
642
+ return `webmcp_${Date.now().toString(36)}_${fallbackCounter}_${Math.random().toString(36).slice(2, 8)}`;
643
+ }
644
+ function isDenyResult(r) {
645
+ return !!r && typeof r === "object" && r.action === "deny";
646
+ }
647
+ async function runErrorHooks(hooks, ctx) {
648
+ if (!hooks || hooks.length === 0) return;
649
+ for (const h of hooks) {
650
+ try {
651
+ await h(ctx);
652
+ } catch {
653
+ }
654
+ }
655
+ }
656
+ async function runDeniedHooks(hooks, ctx) {
657
+ if (!hooks || hooks.length === 0) return;
658
+ for (const h of hooks) {
659
+ try {
660
+ await h(ctx);
661
+ } catch {
662
+ }
663
+ }
664
+ }
665
+ function createHookedExecute(fn, tool, contract, options) {
666
+ return async (args, _ctx) => {
667
+ const invocationId = genInvocationId();
668
+ const metadata = {};
669
+ const signal = tool.__activeSignal ?? createNeverAbortedSignal();
670
+ const merged = options.getHooks();
671
+ let currentInput = args;
672
+ const base = { invocationId, tool, contract, signal, metadata };
673
+ const beforeHooks = merged.before ?? [];
674
+ for (const hook of beforeHooks) {
675
+ if (signal.aborted) {
676
+ const abortErr = new DOMException("Aborted", "AbortError");
677
+ await runErrorHooks(merged.error, {
678
+ ...base,
679
+ input: currentInput,
680
+ error: abortErr
681
+ });
682
+ return normalizeError(abortErr);
683
+ }
684
+ let res;
685
+ try {
686
+ const ctx = { ...base, input: currentInput };
687
+ res = await hook(ctx);
688
+ } catch (hookErr) {
689
+ await runErrorHooks(merged.error, {
690
+ ...base,
691
+ input: currentInput,
692
+ error: hookErr
693
+ });
694
+ return normalizeError(hookErr);
695
+ }
696
+ if (res != null && typeof res === "object") {
697
+ if (isDenyResult(res)) {
698
+ const reason = res.message;
699
+ const code = res.code;
700
+ await runDeniedHooks(merged.denied, {
701
+ ...base,
702
+ input: currentInput,
703
+ reason,
704
+ code
705
+ });
706
+ const msg = reason ?? "Tool execution denied";
707
+ return {
708
+ content: [{ type: "text", text: `Denied: ${msg}` }],
709
+ isError: true,
710
+ ...code ? { code } : {}
711
+ };
712
+ }
713
+ if ("input" in res && res.input !== void 0) {
714
+ currentInput = res.input;
715
+ }
716
+ }
717
+ }
718
+ if (signal.aborted) {
719
+ const abortErr = new DOMException("Aborted", "AbortError");
720
+ await runErrorHooks(merged.error, {
721
+ ...base,
722
+ input: currentInput,
723
+ error: abortErr
724
+ });
725
+ return normalizeError(abortErr);
726
+ }
727
+ if (options.validate) {
728
+ try {
729
+ options.validate(currentInput);
730
+ } catch (valErr) {
731
+ await runErrorHooks(merged.error, {
732
+ ...base,
733
+ input: currentInput,
734
+ error: valErr
735
+ });
736
+ return normalizeError(valErr);
737
+ }
738
+ }
739
+ let rawOutput;
740
+ try {
741
+ rawOutput = await fn(currentInput);
742
+ } catch (fnErr) {
743
+ await runErrorHooks(merged.error, {
744
+ ...base,
745
+ input: currentInput,
746
+ error: fnErr
747
+ });
748
+ return normalizeError(fnErr);
749
+ }
750
+ let currentOutput = rawOutput;
751
+ const afterHooks = merged.after ?? [];
752
+ for (const hook of afterHooks) {
753
+ if (signal.aborted) ;
754
+ try {
755
+ const ctx = {
756
+ ...base,
757
+ input: currentInput,
758
+ output: currentOutput
759
+ };
760
+ const res = await hook(ctx);
761
+ if (res != null && typeof res === "object" && "output" in res && res.output !== void 0) {
762
+ currentOutput = res.output;
763
+ }
764
+ } catch (hookErr) {
765
+ await runErrorHooks(merged.error, {
766
+ ...base,
767
+ input: currentInput,
768
+ error: hookErr
769
+ });
770
+ return normalizeError(hookErr);
771
+ }
772
+ }
773
+ return normalizeResult(currentOutput);
774
+ };
775
+ }
776
+ function createNeverAbortedSignal() {
777
+ try {
778
+ return new AbortController().signal;
779
+ } catch {
780
+ return {
781
+ aborted: false,
782
+ addEventListener() {
783
+ },
784
+ removeEventListener() {
785
+ },
786
+ dispatchEvent() {
787
+ return false;
788
+ },
789
+ onabort: null,
790
+ reason: void 0,
791
+ throwIfAborted() {
792
+ }
793
+ };
794
+ }
795
+ }
796
+
797
+ // src/hooks/config.ts
798
+ var GLOBAL_KEY = "__simpleWebmcp_hooks";
799
+ function getStore() {
800
+ const g = globalThis;
801
+ if (!g[GLOBAL_KEY]) {
802
+ g[GLOBAL_KEY] = { hooks: {} };
803
+ }
804
+ return g[GLOBAL_KEY];
805
+ }
806
+ function getGlobalHooks() {
807
+ return getStore().hooks ?? {};
808
+ }
809
+ function configureWebMCP(opts) {
810
+ const store = getStore();
811
+ if (!opts.hooks) return;
812
+ if (opts.replace) {
813
+ store.hooks = normalizeHooks(opts.hooks);
814
+ return;
815
+ }
816
+ store.hooks = mergeHooks(store.hooks, opts.hooks);
817
+ }
818
+ function resetGlobalHooks() {
819
+ const store = getStore();
820
+ store.hooks = {};
821
+ }
822
+ function normalizeHooks(hooks) {
823
+ return {
824
+ before: hooks.before ? [...hooks.before] : void 0,
825
+ after: hooks.after ? [...hooks.after] : void 0,
826
+ error: hooks.error ? [...hooks.error] : void 0,
827
+ denied: hooks.denied ? [...hooks.denied] : void 0
828
+ };
829
+ }
830
+ function mergeHooks(a, b) {
831
+ if (!a && !b) return {};
832
+ if (!a) return normalizeHooks(b);
833
+ if (!b) return a;
834
+ return {
835
+ before: [...a.before ?? [], ...b.before ?? []],
836
+ after: [...a.after ?? [], ...b.after ?? []],
837
+ error: [...a.error ?? [], ...b.error ?? []],
838
+ denied: [...a.denied ?? [], ...b.denied ?? []]
839
+ };
840
+ }
841
+ function mergeHooksOrdered(opts) {
842
+ const globalHooks = opts.globalHooks ?? {};
843
+ const scopedHooks = opts.scopedHooks ?? {};
844
+ const toolHooks = opts.toolHooks ?? {};
845
+ const before = [
846
+ ...globalHooks.before ?? [],
847
+ ...scopedHooks.before ?? [],
848
+ ...toolHooks.before ?? []
849
+ ];
850
+ const after = [
851
+ ...toolHooks.after ?? [],
852
+ ...scopedHooks.after ?? [],
853
+ ...globalHooks.after ?? []
854
+ ];
855
+ const error = [
856
+ ...toolHooks.error ?? [],
857
+ ...scopedHooks.error ?? [],
858
+ ...globalHooks.error ?? []
859
+ ];
860
+ const denied = [
861
+ ...toolHooks.denied ?? [],
862
+ ...scopedHooks.denied ?? [],
863
+ ...globalHooks.denied ?? []
864
+ ];
865
+ const out = {};
866
+ if (before.length) out.before = before;
867
+ if (after.length) out.after = after;
868
+ if (error.length) out.error = error;
869
+ if (denied.length) out.denied = denied;
870
+ return out;
871
+ }
872
+
641
873
  // src/webmcp.ts
642
874
  function resolveScope(opts) {
643
875
  if (opts?.scope) return opts.scope;
@@ -651,8 +883,24 @@ function webmcp(fn, options) {
651
883
  const anyFn = fn;
652
884
  if (anyFn.__webmcpBrand === true && anyFn.definition) {
653
885
  if (!options || Object.keys(options).length === 0) return anyFn;
886
+ const prevOpts = anyFn.__webmcpOptions || {};
887
+ const mergedOpts = { ...prevOpts, ...options };
888
+ if (prevOpts.hooks || options?.hooks) {
889
+ const prevHooks = prevOpts.hooks ?? {};
890
+ const nextHooks = options.hooks ?? {};
891
+ mergedOpts.hooks = {
892
+ before: [...prevHooks.before ?? [], ...nextHooks.before ?? []],
893
+ after: [...prevHooks.after ?? [], ...nextHooks.after ?? []],
894
+ error: [...prevHooks.error ?? [], ...nextHooks.error ?? []],
895
+ denied: [...prevHooks.denied ?? [], ...nextHooks.denied ?? []]
896
+ };
897
+ for (const k of ["before", "after", "error", "denied"]) {
898
+ if (mergedOpts.hooks[k]?.length === 0) delete mergedOpts.hooks[k];
899
+ }
900
+ if (mergedOpts.hooks && Object.keys(mergedOpts.hooks).length === 0) delete mergedOpts.hooks;
901
+ }
654
902
  const original = anyFn.__fn ?? fn;
655
- return webmcp(original, { ...anyFn.__webmcpOptions || {}, ...options });
903
+ return webmcp(original, mergedOpts);
656
904
  }
657
905
  const name = options?.name ?? toSnakeCase(getFunctionName(fn));
658
906
  if (!name) throw new ConfigurationError('Tool name could not be inferred \u2014 pass {name:"my_tool"}');
@@ -705,26 +953,43 @@ function webmcp(fn, options) {
705
953
  let registrationPromise = null;
706
954
  let unregisterFn = null;
707
955
  let activeController = null;
708
- const wrappedExec = wrapExecute(fn);
956
+ const toolHooks = options?.hooks ? { ...options.hooks } : void 0;
709
957
  const toolWrapper = wrapper;
710
958
  toolWrapper.__webmcpBrand = true;
711
959
  toolWrapper.__fn = fn;
712
960
  toolWrapper.__webmcpOptions = options;
713
961
  if (standard) toolWrapper.__standardSchema = standard;
714
962
  if (outNorm.standard) toolWrapper.__outputStandardSchema = outNorm.standard;
963
+ if (toolHooks) toolWrapper.__hooks = toolHooks;
715
964
  toolWrapper.tool = contract;
716
965
  toolWrapper.definition = contract;
966
+ const hookedExec = createHookedExecute(fn, toolWrapper, contract, {
967
+ getHooks: () => {
968
+ const globalHooks = getGlobalHooks();
969
+ const scopeHooks = toolWrapper.__scopeHooks;
970
+ return mergeHooksOrdered({ globalHooks, scopedHooks: scopeHooks, toolHooks });
971
+ },
972
+ validate: standard ? (input) => {
973
+ const res = standard["~standard"].validate(input);
974
+ if ("issues" in res && res.issues && res.issues.length > 0) {
975
+ const msg = res.issues.map((i) => i.message).join("; ");
976
+ throw new ValidationError(`Validation failed: ${msg}`);
977
+ }
978
+ } : void 0
979
+ });
717
980
  Object.defineProperty(toolWrapper, "status", {
718
981
  get() {
719
982
  return status;
720
983
  },
721
- enumerable: true
984
+ enumerable: true,
985
+ configurable: true
722
986
  });
723
987
  Object.defineProperty(toolWrapper, "registration", {
724
988
  get() {
725
989
  return registrationPromise;
726
990
  },
727
- enumerable: true
991
+ enumerable: true,
992
+ configurable: true
728
993
  });
729
994
  toolWrapper.isRegistered = () => status === "registered";
730
995
  toolWrapper.register = async (opts) => {
@@ -735,14 +1000,25 @@ function webmcp(fn, options) {
735
1000
  return () => {
736
1001
  };
737
1002
  }
1003
+ if (!isWebMCPSupported()) {
1004
+ status = "unsupported";
1005
+ return () => {
1006
+ status = "unregistered";
1007
+ };
1008
+ }
738
1009
  status = "registering";
739
1010
  const controller = new AbortController();
740
1011
  activeController = controller;
1012
+ toolWrapper.__activeSignal = controller.signal;
741
1013
  if (opts?.signal) {
742
1014
  if (opts.signal.aborted) {
743
1015
  controller.abort();
744
1016
  status = "unregistered";
745
1017
  activeController = null;
1018
+ try {
1019
+ delete toolWrapper.__activeSignal;
1020
+ } catch {
1021
+ }
746
1022
  return () => {
747
1023
  };
748
1024
  }
@@ -757,16 +1033,22 @@ function webmcp(fn, options) {
757
1033
  controller.signal.addEventListener("abort", () => {
758
1034
  if (status !== "unregistered") status = "unregistered";
759
1035
  activeController = null;
1036
+ try {
1037
+ delete toolWrapper.__activeSignal;
1038
+ } catch {
1039
+ }
760
1040
  }, { once: true });
761
1041
  try {
762
1042
  const unregister = await registry.register(contract, {
763
1043
  signal: controller.signal,
764
- execute: wrappedExec
1044
+ execute: hookedExec
765
1045
  });
766
1046
  unregisterFn = unregister;
767
1047
  registrationPromise = Promise.resolve();
768
1048
  if (controller.signal.aborted) {
769
1049
  status = "unregistered";
1050
+ } else if (!isWebMCPSupported()) {
1051
+ status = "unsupported";
770
1052
  } else {
771
1053
  status = "registered";
772
1054
  }
@@ -777,10 +1059,23 @@ function webmcp(fn, options) {
777
1059
  }
778
1060
  status = "unregistered";
779
1061
  activeController = null;
1062
+ try {
1063
+ delete toolWrapper.__activeSignal;
1064
+ } catch {
1065
+ }
780
1066
  unregisterFn = null;
781
1067
  };
782
1068
  } catch (err) {
783
- status = "error";
1069
+ if (err instanceof NotSupportedError || err?.code === "NOT_SUPPORTED" || err?.name === "NotSupportedError") {
1070
+ status = "unsupported";
1071
+ } else {
1072
+ status = "error";
1073
+ }
1074
+ activeController = null;
1075
+ try {
1076
+ delete toolWrapper.__activeSignal;
1077
+ } catch {
1078
+ }
784
1079
  registrationPromise = Promise.reject(err);
785
1080
  if (err instanceof SimpleWebMCPError) throw err;
786
1081
  throw err;
@@ -799,6 +1094,10 @@ function webmcp(fn, options) {
799
1094
  }
800
1095
  status = "unregistered";
801
1096
  activeController = null;
1097
+ try {
1098
+ delete toolWrapper.__activeSignal;
1099
+ } catch {
1100
+ }
802
1101
  unregisterFn = null;
803
1102
  registrationPromise = null;
804
1103
  };
@@ -817,6 +1116,9 @@ function webmcp(fn, options) {
817
1116
  webmcp.global = function global(fn, opts) {
818
1117
  return webmcp(fn, { ...opts, global: true });
819
1118
  };
1119
+ webmcp.configure = configureWebMCP;
1120
+ webmcp.getGlobalHooks = getGlobalHooks;
1121
+ webmcp.resetGlobalHooks = resetGlobalHooks;
820
1122
  webmcp.isWebMCPTool = function isWebMCPTool(v) {
821
1123
  return !!v?.__webmcpBrand;
822
1124
  };
@@ -834,14 +1136,21 @@ exports.SimpleWebMCPError = SimpleWebMCPError;
834
1136
  exports.ValidationError = ValidationError;
835
1137
  exports.applyFieldsPatch = applyFieldsPatch;
836
1138
  exports.buildFinalInputSchema = buildFinalInputSchema;
1139
+ exports.configureWebMCP = configureWebMCP;
1140
+ exports.createHookedExecute = createHookedExecute;
837
1141
  exports.default = webmcp_default;
1142
+ exports.genInvocationId = genInvocationId;
1143
+ exports.getGlobalHooks = getGlobalHooks;
838
1144
  exports.getModelContext = getModelContext;
839
1145
  exports.getRegistry = getRegistry;
840
1146
  exports.inferRuntime = inferRuntime;
841
1147
  exports.isWebMCPSupported = isWebMCPSupported;
1148
+ exports.mergeHooks = mergeHooks;
1149
+ exports.mergeHooksOrdered = mergeHooksOrdered;
842
1150
  exports.normalizeError = normalizeError;
843
1151
  exports.normalizeResult = normalizeResult;
844
1152
  exports.registry = registry;
1153
+ exports.resetGlobalHooks = resetGlobalHooks;
845
1154
  exports.toSnakeCase = toSnakeCase;
846
1155
  exports.webmcp = webmcp;
847
1156
  exports.wrapExecute = wrapExecute;