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/index.d.ts CHANGED
@@ -1,9 +1,65 @@
1
- import { a as WebMCPOptions, W as WebMCPTool, M as ModelContextLike, J as JsonSchema, F as FieldDefOrSchema, S as StandardSchemaV1 } from './types-DH78HDAE.js';
2
- export { b as FieldDef, R as RegistrationStatus, T as ToolContract, c as WebMCPAnnotations } from './types-DH78HDAE.js';
3
- export { g as getRegistry, r as registry } from './registry-C6Pz_Rja.js';
1
+ import { b as WebMCPHooks, a as WebMCPOptions, W as WebMCPTool, T as ToolContract, M as ModelContextLike, J as JsonSchema, F as FieldDefOrSchema, S as StandardSchemaV1 } from './types-CF8kTj5O.js';
2
+ export { A as AfterContext, c as AfterHook, B as BeforeContext, d as BeforeHook, D as DeniedContext, e as DeniedHook, E as ErrorContext, f as ErrorHook, g as FieldDef, R as RegistrationStatus, h as WebMCPAnnotations } from './types-CF8kTj5O.js';
3
+ export { g as getRegistry, r as registry } from './registry-Dscedahn.js';
4
+
5
+ declare function getGlobalHooks(): WebMCPHooks<any>;
6
+ declare function configureWebMCP(opts: {
7
+ hooks?: WebMCPHooks<any>;
8
+ replace?: boolean;
9
+ }): void;
10
+ declare function resetGlobalHooks(): void;
11
+ declare function mergeHooks<F extends (...args: any) => any>(a: WebMCPHooks<any> | undefined, b: WebMCPHooks<F> | undefined): WebMCPHooks<F>;
12
+ /**
13
+ * Merge with correct ordering semantics:
14
+ * before: global → scoped → tool
15
+ * after: tool → scoped → global (onion)
16
+ * error/denied: tool → scoped → global
17
+ */
18
+ declare function mergeHooksOrdered<F extends (...args: any) => any>(opts: {
19
+ globalHooks?: WebMCPHooks<any>;
20
+ scopedHooks?: WebMCPHooks<any>;
21
+ toolHooks?: WebMCPHooks<F>;
22
+ }): WebMCPHooks<F>;
4
23
 
5
24
  declare function webmcp<F extends (...args: any) => any>(fn: F, options?: WebMCPOptions<F>): WebMCPTool<F>;
6
25
 
26
+ /**
27
+ * Normalize execute return values per WebMCP spec.
28
+ * WebMCP expects { content: [{type:'text', text: ...}], isError? }
29
+ * We allow fn to return string, object, or already-normalized shape.
30
+ */
31
+ type NormalizedResult = {
32
+ content: Array<{
33
+ type: 'text';
34
+ text: string;
35
+ } | {
36
+ type: string;
37
+ [k: string]: unknown;
38
+ }>;
39
+ isError?: boolean;
40
+ [k: string]: unknown;
41
+ };
42
+ declare function normalizeResult(value: unknown): NormalizedResult;
43
+ declare function normalizeError(err: unknown): NormalizedResult;
44
+ /**
45
+ * Wrap an execute fn to handle:
46
+ * - args: object (WebMCP passes single object) vs spread
47
+ * - async errors
48
+ * - result normalization
49
+ * For 0.1 we assume fn expects single object param (preferred).
50
+ * For multi-arg legacy, user should use webmcp.bind (deferred) — here we provide auto-spread fallback heuristic.
51
+ */
52
+ declare function wrapExecute<F extends (...args: any) => any>(fn: F, opts?: {
53
+ argMode?: 'object' | 'spread';
54
+ }): (args: unknown, ctx?: unknown) => Promise<NormalizedResult>;
55
+
56
+ declare function genInvocationId(): string;
57
+ type CreateHookedExecuteOptions<F extends (...args: any) => any> = {
58
+ getHooks: () => WebMCPHooks<F>;
59
+ validate?: (input: unknown) => void;
60
+ };
61
+ declare function createHookedExecute<F extends (...args: any) => any>(fn: F, tool: WebMCPTool<F>, contract: ToolContract, options: CreateHookedExecuteOptions<F>): (args: unknown, ctx?: unknown) => Promise<NormalizedResult>;
62
+
7
63
  /**
8
64
  * Base error for simple-webmcp
9
65
  */
@@ -56,36 +112,6 @@ declare function toSnakeCase(name: string): string;
56
112
  declare function isWebMCPSupported(): boolean;
57
113
  declare function getModelContext(): ModelContextLike | null;
58
114
 
59
- /**
60
- * Normalize execute return values per WebMCP spec.
61
- * WebMCP expects { content: [{type:'text', text: ...}], isError? }
62
- * We allow fn to return string, object, or already-normalized shape.
63
- */
64
- type NormalizedResult = {
65
- content: Array<{
66
- type: 'text';
67
- text: string;
68
- } | {
69
- type: string;
70
- [k: string]: unknown;
71
- }>;
72
- isError?: boolean;
73
- [k: string]: unknown;
74
- };
75
- declare function normalizeResult(value: unknown): NormalizedResult;
76
- declare function normalizeError(err: unknown): NormalizedResult;
77
- /**
78
- * Wrap an execute fn to handle:
79
- * - args: object (WebMCP passes single object) vs spread
80
- * - async errors
81
- * - result normalization
82
- * For 0.1 we assume fn expects single object param (preferred).
83
- * For multi-arg legacy, user should use webmcp.bind (deferred) — here we provide auto-spread fallback heuristic.
84
- */
85
- declare function wrapExecute<F extends (...args: any) => any>(fn: F, opts?: {
86
- argMode?: 'object' | 'spread';
87
- }): (args: unknown, ctx?: unknown) => Promise<NormalizedResult>;
88
-
89
115
  /**
90
116
  * Runtime inference — best-effort, confidence: low (§11).
91
117
  * We cannot know types from JS alone. We return placeholders and defaults detection.
@@ -133,4 +159,4 @@ declare function buildFinalInputSchema(opts: {
133
159
 
134
160
  declare const SDK_VERSION = "0.1.0";
135
161
 
136
- export { ConfigurationError, FieldDefOrSchema, JsonSchema, NotAllowedError, NotSupportedError, RegistrationError, SDK_VERSION, SimpleWebMCPError, StandardSchemaV1, ValidationError, WebMCPOptions, WebMCPTool, applyFieldsPatch, buildFinalInputSchema, webmcp as default, getModelContext, inferRuntime, isWebMCPSupported, normalizeError, normalizeResult, toSnakeCase, webmcp, wrapExecute };
162
+ export { ConfigurationError, FieldDefOrSchema, JsonSchema, NotAllowedError, NotSupportedError, RegistrationError, SDK_VERSION, SimpleWebMCPError, StandardSchemaV1, ToolContract, ValidationError, WebMCPHooks, WebMCPOptions, WebMCPTool, applyFieldsPatch, buildFinalInputSchema, configureWebMCP, createHookedExecute, webmcp as default, genInvocationId, getGlobalHooks, getModelContext, inferRuntime, isWebMCPSupported, mergeHooks, mergeHooksOrdered, normalizeError, normalizeResult, resetGlobalHooks, toSnakeCase, webmcp, wrapExecute };
package/dist/index.js CHANGED
@@ -590,7 +590,12 @@ function normalizeResult(value) {
590
590
  }
591
591
  }
592
592
  function normalizeError(err) {
593
- 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
+ }
594
599
  try {
595
600
  return JSON.stringify(err);
596
601
  } catch {
@@ -620,6 +625,247 @@ function wrapExecute(fn, opts) {
620
625
  };
621
626
  }
622
627
 
628
+ // src/hooks/engine.ts
629
+ var fallbackCounter = 0;
630
+ function genInvocationId() {
631
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
632
+ return crypto.randomUUID();
633
+ }
634
+ return fallbackInvocationId();
635
+ }
636
+ function fallbackInvocationId() {
637
+ fallbackCounter = (fallbackCounter + 1) % Number.MAX_SAFE_INTEGER;
638
+ return `webmcp_${Date.now().toString(36)}_${fallbackCounter}_${Math.random().toString(36).slice(2, 8)}`;
639
+ }
640
+ function isDenyResult(r) {
641
+ return !!r && typeof r === "object" && r.action === "deny";
642
+ }
643
+ async function runErrorHooks(hooks, ctx) {
644
+ if (!hooks || hooks.length === 0) return;
645
+ for (const h of hooks) {
646
+ try {
647
+ await h(ctx);
648
+ } catch {
649
+ }
650
+ }
651
+ }
652
+ async function runDeniedHooks(hooks, ctx) {
653
+ if (!hooks || hooks.length === 0) return;
654
+ for (const h of hooks) {
655
+ try {
656
+ await h(ctx);
657
+ } catch {
658
+ }
659
+ }
660
+ }
661
+ function createHookedExecute(fn, tool, contract, options) {
662
+ return async (args, _ctx) => {
663
+ const invocationId = genInvocationId();
664
+ const metadata = {};
665
+ const signal = tool.__activeSignal ?? createNeverAbortedSignal();
666
+ const merged = options.getHooks();
667
+ let currentInput = args;
668
+ const base = { invocationId, tool, contract, signal, metadata };
669
+ const beforeHooks = merged.before ?? [];
670
+ for (const hook of beforeHooks) {
671
+ if (signal.aborted) {
672
+ const abortErr = new DOMException("Aborted", "AbortError");
673
+ await runErrorHooks(merged.error, {
674
+ ...base,
675
+ input: currentInput,
676
+ error: abortErr
677
+ });
678
+ return normalizeError(abortErr);
679
+ }
680
+ let res;
681
+ try {
682
+ const ctx = { ...base, input: currentInput };
683
+ res = await hook(ctx);
684
+ } catch (hookErr) {
685
+ await runErrorHooks(merged.error, {
686
+ ...base,
687
+ input: currentInput,
688
+ error: hookErr
689
+ });
690
+ return normalizeError(hookErr);
691
+ }
692
+ if (res != null && typeof res === "object") {
693
+ if (isDenyResult(res)) {
694
+ const reason = res.message;
695
+ const code = res.code;
696
+ await runDeniedHooks(merged.denied, {
697
+ ...base,
698
+ input: currentInput,
699
+ reason,
700
+ code
701
+ });
702
+ const msg = reason ?? "Tool execution denied";
703
+ return {
704
+ content: [{ type: "text", text: `Denied: ${msg}` }],
705
+ isError: true,
706
+ ...code ? { code } : {}
707
+ };
708
+ }
709
+ if ("input" in res && res.input !== void 0) {
710
+ currentInput = res.input;
711
+ }
712
+ }
713
+ }
714
+ if (signal.aborted) {
715
+ const abortErr = new DOMException("Aborted", "AbortError");
716
+ await runErrorHooks(merged.error, {
717
+ ...base,
718
+ input: currentInput,
719
+ error: abortErr
720
+ });
721
+ return normalizeError(abortErr);
722
+ }
723
+ if (options.validate) {
724
+ try {
725
+ options.validate(currentInput);
726
+ } catch (valErr) {
727
+ await runErrorHooks(merged.error, {
728
+ ...base,
729
+ input: currentInput,
730
+ error: valErr
731
+ });
732
+ return normalizeError(valErr);
733
+ }
734
+ }
735
+ let rawOutput;
736
+ try {
737
+ rawOutput = await fn(currentInput);
738
+ } catch (fnErr) {
739
+ await runErrorHooks(merged.error, {
740
+ ...base,
741
+ input: currentInput,
742
+ error: fnErr
743
+ });
744
+ return normalizeError(fnErr);
745
+ }
746
+ let currentOutput = rawOutput;
747
+ const afterHooks = merged.after ?? [];
748
+ for (const hook of afterHooks) {
749
+ if (signal.aborted) ;
750
+ try {
751
+ const ctx = {
752
+ ...base,
753
+ input: currentInput,
754
+ output: currentOutput
755
+ };
756
+ const res = await hook(ctx);
757
+ if (res != null && typeof res === "object" && "output" in res && res.output !== void 0) {
758
+ currentOutput = res.output;
759
+ }
760
+ } catch (hookErr) {
761
+ await runErrorHooks(merged.error, {
762
+ ...base,
763
+ input: currentInput,
764
+ error: hookErr
765
+ });
766
+ return normalizeError(hookErr);
767
+ }
768
+ }
769
+ return normalizeResult(currentOutput);
770
+ };
771
+ }
772
+ function createNeverAbortedSignal() {
773
+ try {
774
+ return new AbortController().signal;
775
+ } catch {
776
+ return {
777
+ aborted: false,
778
+ addEventListener() {
779
+ },
780
+ removeEventListener() {
781
+ },
782
+ dispatchEvent() {
783
+ return false;
784
+ },
785
+ onabort: null,
786
+ reason: void 0,
787
+ throwIfAborted() {
788
+ }
789
+ };
790
+ }
791
+ }
792
+
793
+ // src/hooks/config.ts
794
+ var GLOBAL_KEY = "__simpleWebmcp_hooks";
795
+ function getStore() {
796
+ const g = globalThis;
797
+ if (!g[GLOBAL_KEY]) {
798
+ g[GLOBAL_KEY] = { hooks: {} };
799
+ }
800
+ return g[GLOBAL_KEY];
801
+ }
802
+ function getGlobalHooks() {
803
+ return getStore().hooks ?? {};
804
+ }
805
+ function configureWebMCP(opts) {
806
+ const store = getStore();
807
+ if (!opts.hooks) return;
808
+ if (opts.replace) {
809
+ store.hooks = normalizeHooks(opts.hooks);
810
+ return;
811
+ }
812
+ store.hooks = mergeHooks(store.hooks, opts.hooks);
813
+ }
814
+ function resetGlobalHooks() {
815
+ const store = getStore();
816
+ store.hooks = {};
817
+ }
818
+ function normalizeHooks(hooks) {
819
+ return {
820
+ before: hooks.before ? [...hooks.before] : void 0,
821
+ after: hooks.after ? [...hooks.after] : void 0,
822
+ error: hooks.error ? [...hooks.error] : void 0,
823
+ denied: hooks.denied ? [...hooks.denied] : void 0
824
+ };
825
+ }
826
+ function mergeHooks(a, b) {
827
+ if (!a && !b) return {};
828
+ if (!a) return normalizeHooks(b);
829
+ if (!b) return a;
830
+ return {
831
+ before: [...a.before ?? [], ...b.before ?? []],
832
+ after: [...a.after ?? [], ...b.after ?? []],
833
+ error: [...a.error ?? [], ...b.error ?? []],
834
+ denied: [...a.denied ?? [], ...b.denied ?? []]
835
+ };
836
+ }
837
+ function mergeHooksOrdered(opts) {
838
+ const globalHooks = opts.globalHooks ?? {};
839
+ const scopedHooks = opts.scopedHooks ?? {};
840
+ const toolHooks = opts.toolHooks ?? {};
841
+ const before = [
842
+ ...globalHooks.before ?? [],
843
+ ...scopedHooks.before ?? [],
844
+ ...toolHooks.before ?? []
845
+ ];
846
+ const after = [
847
+ ...toolHooks.after ?? [],
848
+ ...scopedHooks.after ?? [],
849
+ ...globalHooks.after ?? []
850
+ ];
851
+ const error = [
852
+ ...toolHooks.error ?? [],
853
+ ...scopedHooks.error ?? [],
854
+ ...globalHooks.error ?? []
855
+ ];
856
+ const denied = [
857
+ ...toolHooks.denied ?? [],
858
+ ...scopedHooks.denied ?? [],
859
+ ...globalHooks.denied ?? []
860
+ ];
861
+ const out = {};
862
+ if (before.length) out.before = before;
863
+ if (after.length) out.after = after;
864
+ if (error.length) out.error = error;
865
+ if (denied.length) out.denied = denied;
866
+ return out;
867
+ }
868
+
623
869
  // src/webmcp.ts
624
870
  function resolveScope(opts) {
625
871
  if (opts?.scope) return opts.scope;
@@ -633,8 +879,24 @@ function webmcp(fn, options) {
633
879
  const anyFn = fn;
634
880
  if (anyFn.__webmcpBrand === true && anyFn.definition) {
635
881
  if (!options || Object.keys(options).length === 0) return anyFn;
882
+ const prevOpts = anyFn.__webmcpOptions || {};
883
+ const mergedOpts = { ...prevOpts, ...options };
884
+ if (prevOpts.hooks || options?.hooks) {
885
+ const prevHooks = prevOpts.hooks ?? {};
886
+ const nextHooks = options.hooks ?? {};
887
+ mergedOpts.hooks = {
888
+ before: [...prevHooks.before ?? [], ...nextHooks.before ?? []],
889
+ after: [...prevHooks.after ?? [], ...nextHooks.after ?? []],
890
+ error: [...prevHooks.error ?? [], ...nextHooks.error ?? []],
891
+ denied: [...prevHooks.denied ?? [], ...nextHooks.denied ?? []]
892
+ };
893
+ for (const k of ["before", "after", "error", "denied"]) {
894
+ if (mergedOpts.hooks[k]?.length === 0) delete mergedOpts.hooks[k];
895
+ }
896
+ if (mergedOpts.hooks && Object.keys(mergedOpts.hooks).length === 0) delete mergedOpts.hooks;
897
+ }
636
898
  const original = anyFn.__fn ?? fn;
637
- return webmcp(original, { ...anyFn.__webmcpOptions || {}, ...options });
899
+ return webmcp(original, mergedOpts);
638
900
  }
639
901
  const name = options?.name ?? toSnakeCase(getFunctionName(fn));
640
902
  if (!name) throw new ConfigurationError('Tool name could not be inferred \u2014 pass {name:"my_tool"}');
@@ -687,15 +949,30 @@ function webmcp(fn, options) {
687
949
  let registrationPromise = null;
688
950
  let unregisterFn = null;
689
951
  let activeController = null;
690
- const wrappedExec = wrapExecute(fn);
952
+ const toolHooks = options?.hooks ? { ...options.hooks } : void 0;
691
953
  const toolWrapper = wrapper;
692
954
  toolWrapper.__webmcpBrand = true;
693
955
  toolWrapper.__fn = fn;
694
956
  toolWrapper.__webmcpOptions = options;
695
957
  if (standard) toolWrapper.__standardSchema = standard;
696
958
  if (outNorm.standard) toolWrapper.__outputStandardSchema = outNorm.standard;
959
+ if (toolHooks) toolWrapper.__hooks = toolHooks;
697
960
  toolWrapper.tool = contract;
698
961
  toolWrapper.definition = contract;
962
+ const hookedExec = createHookedExecute(fn, toolWrapper, contract, {
963
+ getHooks: () => {
964
+ const globalHooks = getGlobalHooks();
965
+ const scopeHooks = toolWrapper.__scopeHooks;
966
+ return mergeHooksOrdered({ globalHooks, scopedHooks: scopeHooks, toolHooks });
967
+ },
968
+ validate: standard ? (input) => {
969
+ const res = standard["~standard"].validate(input);
970
+ if ("issues" in res && res.issues && res.issues.length > 0) {
971
+ const msg = res.issues.map((i) => i.message).join("; ");
972
+ throw new ValidationError(`Validation failed: ${msg}`);
973
+ }
974
+ } : void 0
975
+ });
699
976
  Object.defineProperty(toolWrapper, "status", {
700
977
  get() {
701
978
  return status;
@@ -728,11 +1005,16 @@ function webmcp(fn, options) {
728
1005
  status = "registering";
729
1006
  const controller = new AbortController();
730
1007
  activeController = controller;
1008
+ toolWrapper.__activeSignal = controller.signal;
731
1009
  if (opts?.signal) {
732
1010
  if (opts.signal.aborted) {
733
1011
  controller.abort();
734
1012
  status = "unregistered";
735
1013
  activeController = null;
1014
+ try {
1015
+ delete toolWrapper.__activeSignal;
1016
+ } catch {
1017
+ }
736
1018
  return () => {
737
1019
  };
738
1020
  }
@@ -747,11 +1029,15 @@ function webmcp(fn, options) {
747
1029
  controller.signal.addEventListener("abort", () => {
748
1030
  if (status !== "unregistered") status = "unregistered";
749
1031
  activeController = null;
1032
+ try {
1033
+ delete toolWrapper.__activeSignal;
1034
+ } catch {
1035
+ }
750
1036
  }, { once: true });
751
1037
  try {
752
1038
  const unregister = await registry.register(contract, {
753
1039
  signal: controller.signal,
754
- execute: wrappedExec
1040
+ execute: hookedExec
755
1041
  });
756
1042
  unregisterFn = unregister;
757
1043
  registrationPromise = Promise.resolve();
@@ -769,6 +1055,10 @@ function webmcp(fn, options) {
769
1055
  }
770
1056
  status = "unregistered";
771
1057
  activeController = null;
1058
+ try {
1059
+ delete toolWrapper.__activeSignal;
1060
+ } catch {
1061
+ }
772
1062
  unregisterFn = null;
773
1063
  };
774
1064
  } catch (err) {
@@ -777,6 +1067,11 @@ function webmcp(fn, options) {
777
1067
  } else {
778
1068
  status = "error";
779
1069
  }
1070
+ activeController = null;
1071
+ try {
1072
+ delete toolWrapper.__activeSignal;
1073
+ } catch {
1074
+ }
780
1075
  registrationPromise = Promise.reject(err);
781
1076
  if (err instanceof SimpleWebMCPError) throw err;
782
1077
  throw err;
@@ -795,6 +1090,10 @@ function webmcp(fn, options) {
795
1090
  }
796
1091
  status = "unregistered";
797
1092
  activeController = null;
1093
+ try {
1094
+ delete toolWrapper.__activeSignal;
1095
+ } catch {
1096
+ }
798
1097
  unregisterFn = null;
799
1098
  registrationPromise = null;
800
1099
  };
@@ -813,6 +1112,9 @@ function webmcp(fn, options) {
813
1112
  webmcp.global = function global(fn, opts) {
814
1113
  return webmcp(fn, { ...opts, global: true });
815
1114
  };
1115
+ webmcp.configure = configureWebMCP;
1116
+ webmcp.getGlobalHooks = getGlobalHooks;
1117
+ webmcp.resetGlobalHooks = resetGlobalHooks;
816
1118
  webmcp.isWebMCPTool = function isWebMCPTool(v) {
817
1119
  return !!v?.__webmcpBrand;
818
1120
  };
@@ -821,6 +1123,6 @@ var webmcp_default = webmcp;
821
1123
  // src/constants.ts
822
1124
  var SDK_VERSION = "0.1.0";
823
1125
 
824
- export { ConfigurationError, NotAllowedError, NotSupportedError, RegistrationError, SDK_VERSION, SimpleWebMCPError, ValidationError, applyFieldsPatch, buildFinalInputSchema, webmcp_default as default, getModelContext, getRegistry, inferRuntime, isWebMCPSupported, normalizeError, normalizeResult, registry, toSnakeCase, webmcp, wrapExecute };
1126
+ export { ConfigurationError, NotAllowedError, NotSupportedError, RegistrationError, SDK_VERSION, SimpleWebMCPError, ValidationError, applyFieldsPatch, buildFinalInputSchema, configureWebMCP, createHookedExecute, webmcp_default as default, genInvocationId, getGlobalHooks, getModelContext, getRegistry, inferRuntime, isWebMCPSupported, mergeHooks, mergeHooksOrdered, normalizeError, normalizeResult, registry, resetGlobalHooks, toSnakeCase, webmcp, wrapExecute };
825
1127
  //# sourceMappingURL=index.js.map
826
1128
  //# sourceMappingURL=index.js.map