Reference — Hooks
import type {
WebMCPHooks, BeforeHook, AfterHook, ErrorHook, DeniedHook,
BeforeContext, AfterContext, ErrorContext, DeniedContext,
} from 'simple-webmcp';
import { webmcp, configureWebMCP, getGlobalHooks, resetGlobalHooks } from 'simple-webmcp';
import { WebMCPProvider } from 'simple-webmcp/react';For usage and lifecycle, see Guide — Hooks and Analytics Step-by-Step.
WebMCPHooks<F>
type WebMCPHooks<F extends (...args:any)=>any> = {
before?: BeforeHook<F>[]; // (ctx) => {input?} | {action:'deny', message?, code?} | void
after?: AfterHook<F>[]; // (ctx) => {output?} | void (output: Awaited<ReturnType<F>>)
error?: ErrorHook<F>[]; // observational, void
denied?: DeniedHook<F>[]; // observational, void — runs after deny
};- All arrays, order matters. Hooks may be
async. FtiesAfterContext.outputtoAwaited<ReturnType<F>>.inputisunknownin this version (avoid coupling to inference); enrich freely.beforereturning{action:'deny'}stops chain, runsdenied[], returnsisErrorto agent.
Contexts
type HookBaseContext<F> = {
invocationId: string; // crypto.randomUUID() || fallback webmcp_${ts}_${counter}_${rand}
tool: WebMCPTool<F>;
contract: ToolContract; // snapshot {name, description, inputSchema, …}
signal: AbortSignal; // registry AbortController signal, cooperative
metadata: Record<string,unknown>; // mutable shared bag per invocation
};
type BeforeContext<F> = HookBaseContext<F> & { input: unknown };
type AfterContext<F> = HookBaseContext<F> & { input: unknown; output: Awaited<ReturnType<F>> };
type ErrorContext<F> = HookBaseContext<F> & { input: unknown; error: unknown };
type DeniedContext<F> = HookBaseContext<F> & { input: unknown; reason?: string; code?: string };metadata is same reference across all hooks in one invocation — before can set metadata.requestId, after/error/denied can read it.
Hook signatures
type BeforeHook<F> = (ctx: BeforeContext<F>) => MaybePromise<void | {input?:unknown} | {action:'deny', message?:string, code?:string}>;
type AfterHook<F> = (ctx: AfterContext<F>) => MaybePromise<void | {output?: Awaited<ReturnType<F>>}>;
type ErrorHook<F> = (ctx: ErrorContext<F>) => MaybePromise<void>;
type DeniedHook<F> = (ctx: DeniedContext<F>) => MaybePromise<void>;
type MaybePromise<T> = T | Promise<T>;error/denied throws are swallowed; before/after throws trigger error[] then normalizeError.
Global — webmcp.configure
webmcp.configure({ hooks:{ before:[...], after:[...], error:[...], denied:[...] }});
// accumulated: second configure concats unless replace:true
webmcp.configure({ hooks:{ before:[a] }});
webmcp.configure({ hooks:{ before:[b] }}); // => [a,b]
webmcp.configure({ hooks:{ before:[c] }, replace:true }); // => [c]
// named exports (same singleton)
import { configureWebMCP, getGlobalHooks, resetGlobalHooks } from 'simple-webmcp';
configureWebMCP({ hooks });
getGlobalHooks(); // current global hooks
resetGlobalHooks(); // clear — use in testsGlobal hooks are stored in a singleton via webmcp.configure() and merged additively unless replace:true. WebMCPOptions.hooks also re-wraps via concat:
const t1 = webmcp(fn, { hooks:{ before:[a] }});
const t2 = webmcp(t1, { hooks:{ before:[b] }}); // before = [a,b]Scoped — WebMCPProvider
import { WebMCPProvider } from 'simple-webmcp/react'; // 'use client'
<WebMCPProvider hooks={{ before:[addTenant] }}>
<WebMCPProvider hooks={{ before:[addRequestId] }}>{/* before=[addTenant, addRequestId] */}</WebMCPProvider>
</WebMCPProvider>
// also re-exported
import { WebMCPHooksContext, useWebMCPHooksContext } from 'simple-webmcp/react';Nesting merges additively ([...parent.before, ...own.before]). Scoped hooks are stored per-tool at mount; useWebMCP reads context synchronously so first register() sees them. If the same tool is mounted under two providers, last write wins — create separate tool instances for isolation.
Merging utilities
import { mergeHooks, mergeHooksOrdered } from 'simple-webmcp';
mergeHooks(a,b); // concat a+b per phase
mergeHooksOrdered({ globalHooks, scopedHooks, toolHooks });
// => { before:[global,scoped,tool], after:[tool,scoped,global], error:[tool,scoped,global], denied:[tool,scoped,global] }Exposed for testing and custom engines.
Engine
import { createHookedExecute, genInvocationId } from 'simple-webmcp';
const execute = createHookedExecute(fn, tool, contract, {
getHooks: () => mergeHooksOrdered({ globalHooks, scopedHooks, toolHooks }),
validate: (input) => { // after before enrichment, before fn
const r = standard['~standard'].validate(input);
if ('issues' in r) throw new ValidationError(...);
}
});
genInvocationId(); // crypto.randomUUID() || fallbackThe engine is what webmcp(fn) installs as registry.register(..., {execute}). It handles signal.aborted cooperative short-circuit, denied isError, error safe observer, and final normalizeResult.
Ordering summary
| phase | order | note |
|---|---|---|
before | global → scoped → tool | enrichment outward→inward |
after | tool → scoped → global | onion — tool closest to fn |
error/denied | tool → scoped → global | tool-specific first |
Notes
- Hooks only on agent path (
invokeTool/executeTool). Directtool(input)bypasses engine. signalabort duringfndoes not forcibly stopfnunlessfncheckssignal.aborted.
See also
- Guide — Hooks — lifecycle and patterns
- Analytics — Overview — PostHog, Sentry, GA4
- Analytics — Step-by-Step — 4-step setup
- Reference — React —
WebMCPProvider - External: Chrome WebMCP API