Skip to main content

Hook Types

FlowHooksOf

Create typed hook decorators for a specific flow:
The stage names a decorator accepts are the stages of that flow (execute, validateInput, finalize, … for tools:call-tool). A hook method receives the running flow, typed FlowCtxOf<'<flow-name>'>; see Hook Context.

Available Flows

@frontmcp/sdk exports the decorators of the common flows ready to use (const { Will, Did } = ToolHook): Up to 1.8.7, FlowHooksOf('prompts:get-prompt'), FlowHooksOf('prompts:list-prompts') and FlowHooksOf('completion:complete') did not typecheck in a consumer project: those flows’ types were missing from the published type definitions.

@Will (Before Hook)

Execute before a flow stage:
The tool name is ctx.state.required.input.name: the flow state has no toolName key, and ctx.state.get() accepts only the keys the flow declares.

Priority

Lower priority values execute first:

Filter

Conditionally execute hooks:

@Did (After Hook)

Execute after a flow stage. A Did hook runs only when the stage succeeded; to see failures too, use an @Around hook.

@Around (Wrapper Hook)

Wrap stage execution for full control. The stage’s Will hooks run first, next() runs the stage itself, and its Did hooks run after the Around hook returns. An Around hook that does not call next() skips the stage. With several Around hooks on one stage, each next() runs the next one, and the innermost runs the stage. For tools:call-tool, the tool’s result is ctx.state.required.toolContext.output.

@Stage (Stage Step)

Add a step to one of the flow’s stages. The stage must be one the flow declares: a flow runs only the stages in its plan, so a @Stage hook cannot add a new stage. A @Stage hook does not replace the flow’s own step: both run, in priority order (lower first). At equal priority a plugin’s or provider’s step runs before the flow’s own, and a step declared on an entry class runs after it. The value the method returns is ignored. To keep the flow’s own step from running, end the flow with ctx.respond(result) from the @Stage hook, or skip the stage with an @Around hook that does not call next().
ctx.respond() ends the flow with that output: the rest of the stage and the stages after it are skipped, while the stage’s Did hooks and the flow’s cleanup stages still run. In tools:call-tool the output is a complete CallToolResult and does not go through the tool’s output schema. To share a value between the hooks of one call, key it on the flow in a WeakMap, or keep it in a local variable of an @Around hook: the flow state accepts only the keys the flow declares, so ctx.state.set('processedInput', ...) does not compile. The Full Example times calls that way.

Hook Options

A hook of a plugin installed on an app runs, in tools/call, resources/read, prompts/get and completion/complete, only for that app’s entries, unless the plugin has scope: 'server': then its hooks run for every app’s entries. With appliesTo: 'uncovered-apps' it also runs for the entries of any app that has no instance of the same hook (same class and method) of its own or from a server-level plugin. Use it for gates that an entry’s metadata asks for, so the entry is not left ungated when the plugin sits on another app.

Registering Hooks

In Apps

In Plugins

On the Server

Hooks declared on a server-level provider run for every app’s tools, resources and prompts:

On a CONTEXT-Scoped Provider

A hook declared on a provider with scope: ProviderScope.CONTEXT runs on the instance built for the request (or session), the same instance the request’s tools resolve with this.get():
The provider’s class must be known up front (a class provider, not a useFactory one). Up to 1.8.7, hooks on server-level and CONTEXT-scoped providers were silently never registered.

On an Entry Class

A hook declared on a @Tool, @Resource, @Prompt or @Agent class runs only for that entry, on the instance created for each call:
Because instance methods run on that instance, they can only hook stages from the one that builds it on: createToolCallContext, createResourceContext, createPromptContext or createAgentContext (Did and Stage on that stage, and any hook on a later stage).

Static hooks for earlier stages

To hook a stage that runs before the instance exists, such as parseInput, findTool, checkToolAuthorization or checkEntryAuthorities (and findResource, findPrompt, findAgent, checkAgentAuthorization), declare the hook as a static method. A static hook runs without an instance: this is the class, and like every hook it receives the running flow as its argument, so it reads and changes the flow’s state, or throws to refuse the call. It joins a call when the call starts, and only a call for its own entry: a static hook on one tool never runs for another.
A static hook may also hook the stage that builds the instance (Will and Around included) or a later stage; it still runs once per call. Among hooks of the same priority, the class’s static hooks run after the hooks of plugins and providers, as its instance hooks do. An instance method on an earlier stage, or Will or Around on the stage that builds the instance, would never run, so the server refuses to start with InvalidHookFlowError, and the message tells you to declare it static. A list-flow hook (ListToolsHook, ListResourcesHook, ListPromptsHook) is refused on an entry class whether it is static or not: a listing builds no instance and runs for no single entry. Declare list hooks on a provider or a plugin instead. Up to 1.9.3 static hooks on earlier stages were refused too. A @Job class declares hooks for the jobs:execute-job flow its attempts run, with the same rules: instance methods from createJobContext on, static methods from the first stage. A hook it declares for another flow fails startup with InvalidHookFlowError. See Jobs: Hooks. Up to 1.9.3 jobs ran outside any flow and any hook on a @Job class failed startup. Up to 1.8.7, all of these were accepted and silently never ran. Hooks from an app’s providers, and from the plugins (including plugins nested inside them) and plugin providers it lists, run only for that app’s tools, resources and prompts (tools:call-tool, resources:read-resource, prompts:get-prompt, completion:complete), including the ones its adapters and plugins provide, such as the tools an OpenAPI adapter generates. The hooks of a plugin with scope: 'server' are the exception: they run for every app’s entries, the hooks declared on its providers included, while the plugin’s providers and context extensions stay with the app that installs it. Plugins and providers registered on the server (@FrontMcp({ plugins, providers })) apply to every app. Resources and prompts the server serves outside every app, such as the SEP-2640 skill:// resources, run every app’s hooks.

Hook Context

A hook method receives the running flow, typed FlowCtxOf<'<flow-name>'>:
See Hook Context for every member of the flow.

Error Handling

A hook that throws stops the flow, and the error reaches the client like an error from the stage itself. An Around hook sees a failing stage as a rejected next(). It can rethrow, retry by calling next() again, or catch the rejection and return normally. Returning normally handles the failure: the stage counts as successful, so set the output the stage would have produced first. An Around hook skipped by its filter still runs the stage:

Full Example

HookRegistry

Hook registry API

Flow Types

Flow type definitions

@Plugin

Create plugins

Customize Flows

Flow customization guide