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/README.md CHANGED
@@ -30,7 +30,7 @@ function CustomersPage() {
30
30
  }
31
31
  ```
32
32
 
33
- **Framework-agnostic core. React adapter included. More adapters coming.** Works with vanilla JS, TypeScript, Vite — Next.js `server()` is experimental until the `fixtures/next-app` spike proves the Server Action bridge.
33
+ **Framework-agnostic core. React adapter included.** Works with vanilla JS, TypeScript, Vite — Next.js support remains experimental until proven.
34
34
 
35
35
  > **One function. Two interfaces.** Human code `search(input)` and agent `search(input)` — same capability.
36
36
  >
@@ -152,6 +152,27 @@ Verbose 2-line still works: `const t = webmcp(fn); useWebMCP(t)`. Layout-level:
152
152
 
153
153
  `register()` is `async` (`Promise<() => void>`) per current `webmcp-types`; hook maps to `AbortSignal` and dedupes StrictMode. `status` is `'unregistered'|'registering'|'registered'|'unsupported'|'error'` — `supported` and `registered` are mutually exclusive (unsupported never claims registered).
154
154
 
155
+ ### Hooks — before / after / error / denied (HITL)
156
+
157
+ ```ts
158
+ const tool = webmcp(checkout, {
159
+ description: 'Checkout cart',
160
+ hooks: {
161
+ before: [async ({input}) => {
162
+ const ok = await confirm(`Checkout £${total}?`);
163
+ if (!ok) return { action: 'deny', message: 'User declined' };
164
+ }],
165
+ after: [({output}) => ({ output: redact(output) })],
166
+ error: [({error}) => console.warn(error)],
167
+ denied: [({reason}) => analytics.track('denied', {reason})],
168
+ }
169
+ });
170
+ webmcp.configure({ hooks:{ before:[trackInvocation], after:[trackResult] }});
171
+ <WebMCPProvider hooks={{ before:[addTenant] }}><Scope tools={[tool]}>{children}</Scope></WebMCPProvider>
172
+ ```
173
+
174
+ Hooks wrap only the agent `execute` path — `tool({input})` stays pure. Ordering: `before` `global→scoped→tool`, `after` `tool→scoped→global`. Direct + `console` + UI logs in the [demo](/demo) **Hooks & HITL** card. See [Guide — Hooks](https://emingure.github.io/simple-webmcp/guide/hooks) and [Analytics](https://emingure.github.io/simple-webmcp/guide/analytics) (PostHog, Mixpanel, GA4, Sentry, etc.).
175
+
155
176
  ## Customize only what you need
156
177
 
157
178
  ```ts
@@ -178,9 +199,9 @@ webmcp(fn, { fields: { query: z.string().describe('Name') } });
178
199
 
179
200
  **Infer what JavaScript can know at runtime. Get richer TypeScript/JSDoc inference with the optional build plugin.**
180
201
 
181
- *Runtime (0.1)* — best-effort, `confidence:'low'`: parameter names, defaults, destructured keys (`{query, limit=20}` → `query` required, `limit` optional `default:20`), some primitives from literal defaults. `function search(query: string)` alone becomes `{properties:{query:{}}}` — we warn and need `fields`/`schema` or `strict:true` throws.
202
+ *Runtime* — best-effort, `confidence:'low'`: parameter names, defaults, destructured keys (`{query, limit=20}` → `query` required, `limit` optional `default:20`), some primitives from literal defaults. `function search(query: string)` alone becomes `{properties:{query:{}}}` — we warn and need `fields`/`schema` or `strict:true` throws.
182
203
 
183
- *Build (0.2 `simple-webmcp/unplugin`)* Vite/Webpack plugin reads TypeScript types + JSDoc before erasure (same approach `webmcp-nexus` validates with `ts-morph`). Same `webmcp(fn)` call, richer `inputSchema`, no code change.
204
+ *Build* optional `simple-webmcp/unplugin` (Vite/Webpack) reads TypeScript types + JSDoc before erasure. Same `webmcp(fn)` call, richer `inputSchema`, no code change.
184
205
 
185
206
  Progressive:
186
207
 
@@ -223,9 +244,11 @@ Start with one function.
223
244
 
224
245
  See `.agents/skills/webmcp-simple/references/api.md`.
225
246
 
226
- **Core:** `webmcp(fn, opts)` → `WebMCPTool` (callable + `tool`/`definition`/`register`/`status`), `webmcp.global`, `isWebMCPSupported()`, `registry.list()`. Errors: `NotSupportedError` (`unsupported` status, mutually exclusive with `registered`), `NotAllowedError`, `RegistrationError`, `ConfigurationError`.
247
+ **Core:** `webmcp(fn, opts)` → `WebMCPTool` (callable + `tool`/`definition`/`register`/`status`), `webmcp.global`, `webmcp.configure({hooks})`, `isWebMCPSupported()`, `registry.list()`. Errors: `NotSupportedError` (`unsupported` status, mutually exclusive with `registered`), `NotAllowedError`, `RegistrationError`, `ConfigurationError`.
227
248
 
228
- **React:** `useWebMCP(fn, opts)` / `useWebMCP(tool)` `WebMCPTool & status` / `status`, `useTool` alias, `<Scope tools>`.
249
+ **Hooks:** `hooks:{ before:[], after:[], error:[], denied:[] }` on `webmcp(fn,{hooks})`, `webmcp.configure`, and `<WebMCPProvider hooks>`. See [Guide — Hooks](https://emingure.github.io/simple-webmcp/guide/hooks) and [Analytics](https://emingure.github.io/simple-webmcp/guide/analytics) for PostHog/Mixpanel/GA4/Sentry examples. Demo shows live hook log + HITL approval modal.
250
+
251
+ **React:** `useWebMCP(fn, opts)` / `useWebMCP(tool)` → `WebMCPTool & status` / `status`, `useTool` alias, `<Scope tools>`, `<WebMCPProvider hooks>`.
229
252
 
230
253
  **Zod:** `import 'simple-webmcp/zod'` then `schema`/`fields` accept Zod/StandardSchema.
231
254
 
@@ -235,19 +258,14 @@ WebMCP today is `document.modelContext` (Chrome canary, origin trial). This pack
235
258
 
236
259
  ```bash
237
260
  npm run build # tsup ESM+CJS+DTS (core 6.26KB gz, zod 1.40KB separate)
238
- npm test # vitest jsdom — 43 tests
261
+ npm test # vitest jsdom — 47 tests
239
262
  npm run typecheck
240
263
  npm run docs:dev # VitePress
241
264
  ```
242
265
 
243
266
  ## Versioning
244
267
 
245
- * **0.1** `webmcp()` + `useWebMCP`/`useTool` + `Scope` + runtime best-effort fallback + dev shim + 43 tests (this release)
246
- * **0.2** — `simple-webmcp/unplugin` TS/JSDoc inference (Vite/Webpack)
247
- * **0.3** — `simple-webmcp/next` `webmcp.server()` — experimental, gated behind `fixtures/next-app` spike. Next 16 defaults to Turbopack, so no Webpack plugin in 0.1.
248
- * **0.4** — `webmcp.bind()`, DevTools, CLI — deferred
249
-
250
- `outputSchema` is kept internally but not marketed in 0.1 — current browser API does not enforce it like `usewebmcp` notes.
268
+ Follows [Semantic Versioning](https://semver.org/). See [`CHANGELOG.md`](./CHANGELOG.md) for the current `0.2.0` notes and [`RELEASING.md`](./RELEASING.md) for the release process. No future roadmap is promised here track GitHub issues/discussions for what's next.
251
269
 
252
270
  ## Skills (Agent)
253
271
 
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
2
  import { InspectedTool } from './inspect.cjs';
3
- import './types-DH78HDAE.cjs';
4
- import './registry-BcPw1qCf.cjs';
3
+ import './types-CF8kTj5O.cjs';
4
+ import './registry-A4DdpmDn.cjs';
5
5
 
6
6
  type Props = {
7
7
  /** If true, panel is expanded by default */
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
2
  import { InspectedTool } from './inspect.js';
3
- import './types-DH78HDAE.js';
4
- import './registry-C6Pz_Rja.js';
3
+ import './types-CF8kTj5O.js';
4
+ import './registry-Dscedahn.js';
5
5
 
6
6
  type Props = {
7
7
  /** If true, panel is expanded by default */
package/dist/index.cjs CHANGED
@@ -594,7 +594,12 @@ function normalizeResult(value) {
594
594
  }
595
595
  }
596
596
  function normalizeError(err) {
597
- 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
+ }
598
603
  try {
599
604
  return JSON.stringify(err);
600
605
  } catch {
@@ -624,6 +629,247 @@ function wrapExecute(fn, opts) {
624
629
  };
625
630
  }
626
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
+
627
873
  // src/webmcp.ts
628
874
  function resolveScope(opts) {
629
875
  if (opts?.scope) return opts.scope;
@@ -637,8 +883,24 @@ function webmcp(fn, options) {
637
883
  const anyFn = fn;
638
884
  if (anyFn.__webmcpBrand === true && anyFn.definition) {
639
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
+ }
640
902
  const original = anyFn.__fn ?? fn;
641
- return webmcp(original, { ...anyFn.__webmcpOptions || {}, ...options });
903
+ return webmcp(original, mergedOpts);
642
904
  }
643
905
  const name = options?.name ?? toSnakeCase(getFunctionName(fn));
644
906
  if (!name) throw new ConfigurationError('Tool name could not be inferred \u2014 pass {name:"my_tool"}');
@@ -691,15 +953,30 @@ function webmcp(fn, options) {
691
953
  let registrationPromise = null;
692
954
  let unregisterFn = null;
693
955
  let activeController = null;
694
- const wrappedExec = wrapExecute(fn);
956
+ const toolHooks = options?.hooks ? { ...options.hooks } : void 0;
695
957
  const toolWrapper = wrapper;
696
958
  toolWrapper.__webmcpBrand = true;
697
959
  toolWrapper.__fn = fn;
698
960
  toolWrapper.__webmcpOptions = options;
699
961
  if (standard) toolWrapper.__standardSchema = standard;
700
962
  if (outNorm.standard) toolWrapper.__outputStandardSchema = outNorm.standard;
963
+ if (toolHooks) toolWrapper.__hooks = toolHooks;
701
964
  toolWrapper.tool = contract;
702
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
+ });
703
980
  Object.defineProperty(toolWrapper, "status", {
704
981
  get() {
705
982
  return status;
@@ -732,11 +1009,16 @@ function webmcp(fn, options) {
732
1009
  status = "registering";
733
1010
  const controller = new AbortController();
734
1011
  activeController = controller;
1012
+ toolWrapper.__activeSignal = controller.signal;
735
1013
  if (opts?.signal) {
736
1014
  if (opts.signal.aborted) {
737
1015
  controller.abort();
738
1016
  status = "unregistered";
739
1017
  activeController = null;
1018
+ try {
1019
+ delete toolWrapper.__activeSignal;
1020
+ } catch {
1021
+ }
740
1022
  return () => {
741
1023
  };
742
1024
  }
@@ -751,11 +1033,15 @@ function webmcp(fn, options) {
751
1033
  controller.signal.addEventListener("abort", () => {
752
1034
  if (status !== "unregistered") status = "unregistered";
753
1035
  activeController = null;
1036
+ try {
1037
+ delete toolWrapper.__activeSignal;
1038
+ } catch {
1039
+ }
754
1040
  }, { once: true });
755
1041
  try {
756
1042
  const unregister = await registry.register(contract, {
757
1043
  signal: controller.signal,
758
- execute: wrappedExec
1044
+ execute: hookedExec
759
1045
  });
760
1046
  unregisterFn = unregister;
761
1047
  registrationPromise = Promise.resolve();
@@ -773,6 +1059,10 @@ function webmcp(fn, options) {
773
1059
  }
774
1060
  status = "unregistered";
775
1061
  activeController = null;
1062
+ try {
1063
+ delete toolWrapper.__activeSignal;
1064
+ } catch {
1065
+ }
776
1066
  unregisterFn = null;
777
1067
  };
778
1068
  } catch (err) {
@@ -781,6 +1071,11 @@ function webmcp(fn, options) {
781
1071
  } else {
782
1072
  status = "error";
783
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;