simple-webmcp 0.2.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.
package/dist/react.js CHANGED
@@ -1,5 +1,5 @@
1
- import { useMemo, useState, useRef, useEffect } from 'react';
2
- import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
1
+ import { createContext, useContext, useMemo, useState, useRef, useEffect } from 'react';
2
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
3
3
 
4
4
  // src/internal/utils.ts
5
5
  function toSnakeCase(name) {
@@ -79,6 +79,11 @@ var RegistrationError = class extends SimpleWebMCPError {
79
79
  super(message, "REGISTRATION_ERROR", opts?.cause);
80
80
  }
81
81
  };
82
+ var ValidationError = class extends SimpleWebMCPError {
83
+ constructor(message, opts) {
84
+ super(message, "VALIDATION_ERROR", opts?.cause);
85
+ }
86
+ };
82
87
  var ConfigurationError = class extends SimpleWebMCPError {
83
88
  constructor(message, opts) {
84
89
  super(message, "CONFIGURATION_ERROR", opts?.cause);
@@ -585,7 +590,12 @@ function normalizeResult(value) {
585
590
  }
586
591
  }
587
592
  function normalizeError(err) {
588
- const message = err instanceof Error ? err.message : typeof err === "string" ? err : (() => {
593
+ const message = (() => {
594
+ if (err instanceof Error) return err.message;
595
+ if (typeof err === "string") return err;
596
+ if (err && typeof err === "object" && "message" in err && typeof err.message === "string") {
597
+ return err.message;
598
+ }
589
599
  try {
590
600
  return JSON.stringify(err);
591
601
  } catch {
@@ -597,20 +607,247 @@ function normalizeError(err) {
597
607
  isError: true
598
608
  };
599
609
  }
600
- function wrapExecute(fn, opts) {
610
+
611
+ // src/hooks/engine.ts
612
+ var fallbackCounter = 0;
613
+ function genInvocationId() {
614
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
615
+ return crypto.randomUUID();
616
+ }
617
+ return fallbackInvocationId();
618
+ }
619
+ function fallbackInvocationId() {
620
+ fallbackCounter = (fallbackCounter + 1) % Number.MAX_SAFE_INTEGER;
621
+ return `webmcp_${Date.now().toString(36)}_${fallbackCounter}_${Math.random().toString(36).slice(2, 8)}`;
622
+ }
623
+ function isDenyResult(r) {
624
+ return !!r && typeof r === "object" && r.action === "deny";
625
+ }
626
+ async function runErrorHooks(hooks, ctx) {
627
+ if (!hooks || hooks.length === 0) return;
628
+ for (const h of hooks) {
629
+ try {
630
+ await h(ctx);
631
+ } catch {
632
+ }
633
+ }
634
+ }
635
+ async function runDeniedHooks(hooks, ctx) {
636
+ if (!hooks || hooks.length === 0) return;
637
+ for (const h of hooks) {
638
+ try {
639
+ await h(ctx);
640
+ } catch {
641
+ }
642
+ }
643
+ }
644
+ function createHookedExecute(fn, tool, contract, options) {
601
645
  return async (args, _ctx) => {
646
+ const invocationId = genInvocationId();
647
+ const metadata = {};
648
+ const signal = tool.__activeSignal ?? createNeverAbortedSignal();
649
+ const merged = options.getHooks();
650
+ let currentInput = args;
651
+ const base = { invocationId, tool, contract, signal, metadata };
652
+ const beforeHooks = merged.before ?? [];
653
+ for (const hook of beforeHooks) {
654
+ if (signal.aborted) {
655
+ const abortErr = new DOMException("Aborted", "AbortError");
656
+ await runErrorHooks(merged.error, {
657
+ ...base,
658
+ input: currentInput,
659
+ error: abortErr
660
+ });
661
+ return normalizeError(abortErr);
662
+ }
663
+ let res;
664
+ try {
665
+ const ctx = { ...base, input: currentInput };
666
+ res = await hook(ctx);
667
+ } catch (hookErr) {
668
+ await runErrorHooks(merged.error, {
669
+ ...base,
670
+ input: currentInput,
671
+ error: hookErr
672
+ });
673
+ return normalizeError(hookErr);
674
+ }
675
+ if (res != null && typeof res === "object") {
676
+ if (isDenyResult(res)) {
677
+ const reason = res.message;
678
+ const code = res.code;
679
+ await runDeniedHooks(merged.denied, {
680
+ ...base,
681
+ input: currentInput,
682
+ reason,
683
+ code
684
+ });
685
+ const msg = reason ?? "Tool execution denied";
686
+ return {
687
+ content: [{ type: "text", text: `Denied: ${msg}` }],
688
+ isError: true,
689
+ ...code ? { code } : {}
690
+ };
691
+ }
692
+ if ("input" in res && res.input !== void 0) {
693
+ currentInput = res.input;
694
+ }
695
+ }
696
+ }
697
+ if (signal.aborted) {
698
+ const abortErr = new DOMException("Aborted", "AbortError");
699
+ await runErrorHooks(merged.error, {
700
+ ...base,
701
+ input: currentInput,
702
+ error: abortErr
703
+ });
704
+ return normalizeError(abortErr);
705
+ }
706
+ if (options.validate) {
707
+ try {
708
+ options.validate(currentInput);
709
+ } catch (valErr) {
710
+ await runErrorHooks(merged.error, {
711
+ ...base,
712
+ input: currentInput,
713
+ error: valErr
714
+ });
715
+ return normalizeError(valErr);
716
+ }
717
+ }
718
+ let rawOutput;
602
719
  try {
603
- let result;
604
- const mode = opts?.argMode ?? "object";
605
- if (mode === "spread" && args && typeof args === "object" && !Array.isArray(args)) ; else {
606
- result = await fn(args);
720
+ rawOutput = await fn(currentInput);
721
+ } catch (fnErr) {
722
+ await runErrorHooks(merged.error, {
723
+ ...base,
724
+ input: currentInput,
725
+ error: fnErr
726
+ });
727
+ return normalizeError(fnErr);
728
+ }
729
+ let currentOutput = rawOutput;
730
+ const afterHooks = merged.after ?? [];
731
+ for (const hook of afterHooks) {
732
+ if (signal.aborted) ;
733
+ try {
734
+ const ctx = {
735
+ ...base,
736
+ input: currentInput,
737
+ output: currentOutput
738
+ };
739
+ const res = await hook(ctx);
740
+ if (res != null && typeof res === "object" && "output" in res && res.output !== void 0) {
741
+ currentOutput = res.output;
742
+ }
743
+ } catch (hookErr) {
744
+ await runErrorHooks(merged.error, {
745
+ ...base,
746
+ input: currentInput,
747
+ error: hookErr
748
+ });
749
+ return normalizeError(hookErr);
607
750
  }
608
- return normalizeResult(result);
609
- } catch (err) {
610
- return normalizeError(err);
611
751
  }
752
+ return normalizeResult(currentOutput);
612
753
  };
613
754
  }
755
+ function createNeverAbortedSignal() {
756
+ try {
757
+ return new AbortController().signal;
758
+ } catch {
759
+ return {
760
+ aborted: false,
761
+ addEventListener() {
762
+ },
763
+ removeEventListener() {
764
+ },
765
+ dispatchEvent() {
766
+ return false;
767
+ },
768
+ onabort: null,
769
+ reason: void 0,
770
+ throwIfAborted() {
771
+ }
772
+ };
773
+ }
774
+ }
775
+
776
+ // src/hooks/config.ts
777
+ var GLOBAL_KEY = "__simpleWebmcp_hooks";
778
+ function getStore() {
779
+ const g = globalThis;
780
+ if (!g[GLOBAL_KEY]) {
781
+ g[GLOBAL_KEY] = { hooks: {} };
782
+ }
783
+ return g[GLOBAL_KEY];
784
+ }
785
+ function getGlobalHooks() {
786
+ return getStore().hooks ?? {};
787
+ }
788
+ function configureWebMCP(opts) {
789
+ const store = getStore();
790
+ if (!opts.hooks) return;
791
+ if (opts.replace) {
792
+ store.hooks = normalizeHooks(opts.hooks);
793
+ return;
794
+ }
795
+ store.hooks = mergeHooks(store.hooks, opts.hooks);
796
+ }
797
+ function resetGlobalHooks() {
798
+ const store = getStore();
799
+ store.hooks = {};
800
+ }
801
+ function normalizeHooks(hooks) {
802
+ return {
803
+ before: hooks.before ? [...hooks.before] : void 0,
804
+ after: hooks.after ? [...hooks.after] : void 0,
805
+ error: hooks.error ? [...hooks.error] : void 0,
806
+ denied: hooks.denied ? [...hooks.denied] : void 0
807
+ };
808
+ }
809
+ function mergeHooks(a, b) {
810
+ if (!a && !b) return {};
811
+ if (!a) return normalizeHooks(b);
812
+ if (!b) return a;
813
+ return {
814
+ before: [...a.before ?? [], ...b.before ?? []],
815
+ after: [...a.after ?? [], ...b.after ?? []],
816
+ error: [...a.error ?? [], ...b.error ?? []],
817
+ denied: [...a.denied ?? [], ...b.denied ?? []]
818
+ };
819
+ }
820
+ function mergeHooksOrdered(opts) {
821
+ const globalHooks = opts.globalHooks ?? {};
822
+ const scopedHooks = opts.scopedHooks ?? {};
823
+ const toolHooks = opts.toolHooks ?? {};
824
+ const before = [
825
+ ...globalHooks.before ?? [],
826
+ ...scopedHooks.before ?? [],
827
+ ...toolHooks.before ?? []
828
+ ];
829
+ const after = [
830
+ ...toolHooks.after ?? [],
831
+ ...scopedHooks.after ?? [],
832
+ ...globalHooks.after ?? []
833
+ ];
834
+ const error = [
835
+ ...toolHooks.error ?? [],
836
+ ...scopedHooks.error ?? [],
837
+ ...globalHooks.error ?? []
838
+ ];
839
+ const denied = [
840
+ ...toolHooks.denied ?? [],
841
+ ...scopedHooks.denied ?? [],
842
+ ...globalHooks.denied ?? []
843
+ ];
844
+ const out = {};
845
+ if (before.length) out.before = before;
846
+ if (after.length) out.after = after;
847
+ if (error.length) out.error = error;
848
+ if (denied.length) out.denied = denied;
849
+ return out;
850
+ }
614
851
 
615
852
  // src/webmcp.ts
616
853
  function resolveScope(opts) {
@@ -625,8 +862,24 @@ function webmcp(fn, options) {
625
862
  const anyFn = fn;
626
863
  if (anyFn.__webmcpBrand === true && anyFn.definition) {
627
864
  if (!options || Object.keys(options).length === 0) return anyFn;
865
+ const prevOpts = anyFn.__webmcpOptions || {};
866
+ const mergedOpts = { ...prevOpts, ...options };
867
+ if (prevOpts.hooks || options?.hooks) {
868
+ const prevHooks = prevOpts.hooks ?? {};
869
+ const nextHooks = options.hooks ?? {};
870
+ mergedOpts.hooks = {
871
+ before: [...prevHooks.before ?? [], ...nextHooks.before ?? []],
872
+ after: [...prevHooks.after ?? [], ...nextHooks.after ?? []],
873
+ error: [...prevHooks.error ?? [], ...nextHooks.error ?? []],
874
+ denied: [...prevHooks.denied ?? [], ...nextHooks.denied ?? []]
875
+ };
876
+ for (const k of ["before", "after", "error", "denied"]) {
877
+ if (mergedOpts.hooks[k]?.length === 0) delete mergedOpts.hooks[k];
878
+ }
879
+ if (mergedOpts.hooks && Object.keys(mergedOpts.hooks).length === 0) delete mergedOpts.hooks;
880
+ }
628
881
  const original = anyFn.__fn ?? fn;
629
- return webmcp(original, { ...anyFn.__webmcpOptions || {}, ...options });
882
+ return webmcp(original, mergedOpts);
630
883
  }
631
884
  const name = options?.name ?? toSnakeCase(getFunctionName(fn));
632
885
  if (!name) throw new ConfigurationError('Tool name could not be inferred \u2014 pass {name:"my_tool"}');
@@ -679,15 +932,30 @@ function webmcp(fn, options) {
679
932
  let registrationPromise = null;
680
933
  let unregisterFn = null;
681
934
  let activeController = null;
682
- const wrappedExec = wrapExecute(fn);
935
+ const toolHooks = options?.hooks ? { ...options.hooks } : void 0;
683
936
  const toolWrapper = wrapper;
684
937
  toolWrapper.__webmcpBrand = true;
685
938
  toolWrapper.__fn = fn;
686
939
  toolWrapper.__webmcpOptions = options;
687
940
  if (standard) toolWrapper.__standardSchema = standard;
688
941
  if (outNorm.standard) toolWrapper.__outputStandardSchema = outNorm.standard;
942
+ if (toolHooks) toolWrapper.__hooks = toolHooks;
689
943
  toolWrapper.tool = contract;
690
944
  toolWrapper.definition = contract;
945
+ const hookedExec = createHookedExecute(fn, toolWrapper, contract, {
946
+ getHooks: () => {
947
+ const globalHooks = getGlobalHooks();
948
+ const scopeHooks = toolWrapper.__scopeHooks;
949
+ return mergeHooksOrdered({ globalHooks, scopedHooks: scopeHooks, toolHooks });
950
+ },
951
+ validate: standard ? (input) => {
952
+ const res = standard["~standard"].validate(input);
953
+ if ("issues" in res && res.issues && res.issues.length > 0) {
954
+ const msg = res.issues.map((i) => i.message).join("; ");
955
+ throw new ValidationError(`Validation failed: ${msg}`);
956
+ }
957
+ } : void 0
958
+ });
691
959
  Object.defineProperty(toolWrapper, "status", {
692
960
  get() {
693
961
  return status;
@@ -720,11 +988,16 @@ function webmcp(fn, options) {
720
988
  status = "registering";
721
989
  const controller = new AbortController();
722
990
  activeController = controller;
991
+ toolWrapper.__activeSignal = controller.signal;
723
992
  if (opts?.signal) {
724
993
  if (opts.signal.aborted) {
725
994
  controller.abort();
726
995
  status = "unregistered";
727
996
  activeController = null;
997
+ try {
998
+ delete toolWrapper.__activeSignal;
999
+ } catch {
1000
+ }
728
1001
  return () => {
729
1002
  };
730
1003
  }
@@ -739,11 +1012,15 @@ function webmcp(fn, options) {
739
1012
  controller.signal.addEventListener("abort", () => {
740
1013
  if (status !== "unregistered") status = "unregistered";
741
1014
  activeController = null;
1015
+ try {
1016
+ delete toolWrapper.__activeSignal;
1017
+ } catch {
1018
+ }
742
1019
  }, { once: true });
743
1020
  try {
744
1021
  const unregister = await registry.register(contract, {
745
1022
  signal: controller.signal,
746
- execute: wrappedExec
1023
+ execute: hookedExec
747
1024
  });
748
1025
  unregisterFn = unregister;
749
1026
  registrationPromise = Promise.resolve();
@@ -761,6 +1038,10 @@ function webmcp(fn, options) {
761
1038
  }
762
1039
  status = "unregistered";
763
1040
  activeController = null;
1041
+ try {
1042
+ delete toolWrapper.__activeSignal;
1043
+ } catch {
1044
+ }
764
1045
  unregisterFn = null;
765
1046
  };
766
1047
  } catch (err) {
@@ -769,6 +1050,11 @@ function webmcp(fn, options) {
769
1050
  } else {
770
1051
  status = "error";
771
1052
  }
1053
+ activeController = null;
1054
+ try {
1055
+ delete toolWrapper.__activeSignal;
1056
+ } catch {
1057
+ }
772
1058
  registrationPromise = Promise.reject(err);
773
1059
  if (err instanceof SimpleWebMCPError) throw err;
774
1060
  throw err;
@@ -787,6 +1073,10 @@ function webmcp(fn, options) {
787
1073
  }
788
1074
  status = "unregistered";
789
1075
  activeController = null;
1076
+ try {
1077
+ delete toolWrapper.__activeSignal;
1078
+ } catch {
1079
+ }
790
1080
  unregisterFn = null;
791
1081
  registrationPromise = null;
792
1082
  };
@@ -805,9 +1095,30 @@ function webmcp(fn, options) {
805
1095
  webmcp.global = function global(fn, opts) {
806
1096
  return webmcp(fn, { ...opts, global: true });
807
1097
  };
1098
+ webmcp.configure = configureWebMCP;
1099
+ webmcp.getGlobalHooks = getGlobalHooks;
1100
+ webmcp.resetGlobalHooks = resetGlobalHooks;
808
1101
  webmcp.isWebMCPTool = function isWebMCPTool(v) {
809
1102
  return !!v?.__webmcpBrand;
810
1103
  };
1104
+ var WebMCPHooksContext = createContext({});
1105
+ function WebMCPProvider({ hooks, children }) {
1106
+ const parent = useContext(WebMCPHooksContext);
1107
+ const merged = useMemo(() => {
1108
+ if (!hooks) return parent;
1109
+ if (!parent || Object.keys(parent).length === 0) return hooks;
1110
+ return {
1111
+ before: [...parent.before ?? [], ...hooks.before ?? []],
1112
+ after: [...parent.after ?? [], ...hooks.after ?? []],
1113
+ error: [...parent.error ?? [], ...hooks.error ?? []],
1114
+ denied: [...parent.denied ?? [], ...hooks.denied ?? []]
1115
+ };
1116
+ }, [parent, hooks]);
1117
+ return /* @__PURE__ */ jsx(WebMCPHooksContext.Provider, { value: merged, children });
1118
+ }
1119
+ function useWebMCPHooksContext() {
1120
+ return useContext(WebMCPHooksContext);
1121
+ }
811
1122
 
812
1123
  // src/react/useWebMCP.ts
813
1124
  function isTool(v) {
@@ -815,6 +1126,7 @@ function isTool(v) {
815
1126
  }
816
1127
  function useWebMCP(arg, opts) {
817
1128
  const enabled = opts?.enabled ?? true;
1129
+ const scopedHooks = useWebMCPHooksContext();
818
1130
  const isRawFunction = useMemo(() => {
819
1131
  return typeof arg === "function" && !isTool(arg);
820
1132
  }, [arg]);
@@ -836,6 +1148,7 @@ function useWebMCP(arg, opts) {
836
1148
  opts?.global,
837
1149
  opts?.strict,
838
1150
  opts?.outputSchema,
1151
+ opts?.hooks,
839
1152
  opts?.enabled
840
1153
  ]);
841
1154
  const [registered, setRegistered] = useState(false);
@@ -848,6 +1161,31 @@ function useWebMCP(arg, opts) {
848
1161
  }, [supported]);
849
1162
  const toolRef = useRef(tool);
850
1163
  toolRef.current = tool;
1164
+ useEffect(() => {
1165
+ const t = toolRef.current;
1166
+ const hasScoped = scopedHooks && Object.keys(scopedHooks).length > 0;
1167
+ if (hasScoped) t.__scopeHooks = scopedHooks;
1168
+ else if (t.__scopeHooks) delete t.__scopeHooks;
1169
+ return () => {
1170
+ const cur = t.__scopeHooks;
1171
+ if (cur === scopedHooks) {
1172
+ try {
1173
+ delete t.__scopeHooks;
1174
+ } catch {
1175
+ }
1176
+ }
1177
+ };
1178
+ }, [scopedHooks, tool]);
1179
+ if (scopedHooks && Object.keys(scopedHooks).length > 0) {
1180
+ tool.__scopeHooks = scopedHooks;
1181
+ } else if (tool.__scopeHooks) {
1182
+ if (!(scopedHooks && Object.keys(scopedHooks).length > 0)) {
1183
+ try {
1184
+ delete tool.__scopeHooks;
1185
+ } catch {
1186
+ }
1187
+ }
1188
+ }
851
1189
  useEffect(() => {
852
1190
  if (!enabled) {
853
1191
  setRegistered(false);
@@ -938,6 +1276,6 @@ function Scope({ tools, enabled = true, children }) {
938
1276
  }
939
1277
  var WebMCPScope = Scope;
940
1278
 
941
- export { Scope, WebMCPScope, useTool, useWebMCP };
1279
+ export { Scope, WebMCPHooksContext, WebMCPProvider, WebMCPScope, useTool, useWebMCP, useWebMCPHooksContext };
942
1280
  //# sourceMappingURL=react.js.map
943
1281
  //# sourceMappingURL=react.js.map