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 +30 -12
- package/dist/devtools.d.cts +2 -2
- package/dist/devtools.d.ts +2 -2
- package/dist/index.cjs +313 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +60 -34
- package/dist/index.d.ts +60 -34
- package/dist/index.js +307 -5
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.cts +2 -2
- package/dist/inspect.d.ts +2 -2
- package/dist/react.cjs +353 -12
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +23 -2
- package/dist/react.d.ts +23 -2
- package/dist/react.js +353 -15
- package/dist/react.js.map +1 -1
- package/dist/{registry-BcPw1qCf.d.cts → registry-A4DdpmDn.d.cts} +1 -1
- package/dist/{registry-C6Pz_Rja.d.ts → registry-Dscedahn.d.ts} +1 -1
- package/dist/{types-DH78HDAE.d.cts → types-CF8kTj5O.d.cts} +60 -1
- package/dist/{types-DH78HDAE.d.ts → types-CF8kTj5O.d.ts} +60 -1
- package/dist/zod.d.cts +1 -1
- package/dist/zod.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ function CustomersPage() {
|
|
|
30
30
|
}
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
**Framework-agnostic core. React adapter included
|
|
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
|
|
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
|
|
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
|
-
**
|
|
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 —
|
|
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
|
-
|
|
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
|
|
package/dist/devtools.d.cts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { InspectedTool } from './inspect.cjs';
|
|
3
|
-
import './types-
|
|
4
|
-
import './registry-
|
|
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 */
|
package/dist/devtools.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { InspectedTool } from './inspect.js';
|
|
3
|
-
import './types-
|
|
4
|
-
import './registry-
|
|
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 =
|
|
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,
|
|
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
|
|
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:
|
|
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;
|