Plugins and adapters
A plugin adds behaviour that belongs around your tools rather than in one of them: a cache in front of every call, a memory tools can read, an approval step, a feature flag. An adapter generates tools from another description of an API, such as an OpenAPI spec. The FrontMCP team publishes eight plugins and one adapter, each in its own npm package, and you turn one on by listing SomePlugin.init({ ... }) in plugins, or SomeAdapter.init({ ... }) in adapters. This page is the map: what each one is for, where to register it, and what goes wrong. Each has its own page, and @Plugin covers writing your own.
@App({ id, name, tools, plugins: [CachePlugin.init({ ... })], adapters: [OpenapiAdapter.init({ name, ... })] })
@FrontMcp({ info, apps, plugins: [RememberPlugin.init({ ... })] })
Reference
The official plugins and adapters
| Package | Export | What it does | Page |
|---|---|---|---|
@frontmcp/plugin-cache | CachePlugin | Answers repeated tool calls from a store instead of running the tool again. | Cache |
@frontmcp/plugin-remember | RememberPlugin | this.remember, an encrypted key-value memory for tools, and, when you turn them on, four tools that let the model remember things. | Remember |
@frontmcp/plugin-approval | ApprovalPlugin | Stops tools marked with approval until the call is approved. | Approval |
@frontmcp/plugin-feature-flags | FeatureFlagPlugin | Gates tools, resources, prompts and skills behind flags from a static list, Split, LaunchDarkly, Unleash or your own source. | Feature flags |
@frontmcp/plugin-codecall | CodeCallPlugin | Six codecall:* tools with which the model searches your tools and runs short scripts that call them. | CodeCall |
@frontmcp/plugin-skilled-openapi | SkilledOpenApiPlugin | Serves a REST API as skill bundles, with its operations behind a few meta-tools instead of one tool each. | Skilled OpenAPI |
@frontmcp/plugin-dashboard | DashboardPlugin, DashboardApp | A web page and an MCP endpoint that show what the server has. | The dashboard |
@frontmcp/plugin-webmcp | WebMcpPlugin | Offers the tools of a server that runs in a web page to the agents in the user's browser, through WebMCP. | WebMCP |
@frontmcp/adapters | OpenapiAdapter | A tool for each operation in an OpenAPI 3 spec, which calls the API. | OpenAPI adapter |
Packages
Install the packages you use. Each one depends on exactly its own version of @frontmcp/sdk (1.9.4 on @frontmcp/sdk 1.9.4), so keep every @frontmcp/* package on one version:
npm install @frontmcp/plugin-cache @frontmcp/plugin-remember @frontmcp/adapters@frontmcp/plugins installs four of them and re-exports what they export: the cache, CodeCall, dashboard and Remember plugins. It doesn't include the approval, feature flags, Skilled OpenAPI or WebMCP plugins, or the adapters. Importing CachePlugin from @frontmcp/plugins gives the same class as importing it from @frontmcp/plugin-cache, so pick one and use it everywhere. @frontmcp/plugin-codecall also needs @frontmcp/plugin-cache, a peer dependency you install next to it.
Registering a plugin
Put SomePlugin.init(options) in the plugins array of an @App, for that app, or of @FrontMcp, for the whole server:
In @App({ plugins }) | In @FrontMcp({ plugins }) | |
|---|---|---|
| Hooks on calls, like the cache's lookup or an approval check | That app's tools. The approval and feature-flag gates also cover apps that have no such plugin of their own. | Every app's tools |
| Tools the plugin adds, like CodeCall's | Added to that app | Added once, for the server |
Providers the plugin builds from its options, like the one behind this.remember | That app's tools | Every app's tools |
Changed in 1.9: a plugin's providers stay in the app that registered it. Before, they reached every app's tools, so a tool in another app could use this.remember and read the same values. Now that tool gets RememberPlugin is not installed, this.get() throws ProviderNotAvailableError and this.tryGet() returns undefined. Register the plugin on @FrontMcp, or on each app that uses it.
The full rules, for any plugin, are in where you register a plugin. An @Agent has plugins too, for the tools declared inside it: the agent runs those itself, and only its own plugins apply to them. See putting a plugin on an agent, and flagging an agent for an official one.
init()needs no argument when every option has a default.CachePlugin.init(),RememberPlugin.init(),ApprovalPlugin.init()andCodeCallPlugin.init()don't throw: each is the same asinit({}). A required option is still required: feature flags need anadapter(FeatureFlagPlugin.init()throws aFeatureFlagConfigurationError), and Skilled OpenAPI needs asource(init()throws aZodError). The bare class,plugins: [RememberPlugin], is the same asinit()with no argument: it works for the cache, Remember, approval and CodeCall plugins, and a plugin with a required option stops the server from starting with the error itsinit()throws. Changed in 1.9.4: only the cache plugin worked as a bare class. Remember had nothis.remember, the approval and feature flags plugins failed withProvider "…" is not availableonce they were used, and CodeCall left the server with no tools.init()runs when your module is imported, not when the server starts. SeeDynamicPlugin.- An option named like a list field of
@Pluginis that field only when it's an array.init()copies its options onto the plugin's registration, wheretools,resources,prompts,skills,adapters,plugins,exportsandprovidersdescribe the plugin itself. An array registers what it lists. Any other value stays the plugin's option, which is how Remember takestools: { enabled: true }, and a plugin can add tools from it with a staticdynamicTools(options), asdynamicProviders(options)adds providers. The Playground below runs each case. Changed in 1.9: an option namedprovidersthat wasn't an array failedinit()withTypeError: (extraProviders ?? []) is not iterable. - Options named
name,id,descriptionorscopeare only options. They reach the plugin, and don't rename it or change where it applies. Changed in 1.9.2:init()copied them onto the registration, soinit({ name: "eu" })renamed the plugin andinit({ scope: … })changed its install scope.
Registering an adapter
Put SomeAdapter.init(options) in the adapters array of an @App, of a @Plugin, or of @FrontMcp, which serves the adapter's tools from every app. When two adapters make a tool of the same name, both are listed with their adapter's name in front, like pets:getPet and pets-v2:getPet. Changed in 1.9: @FrontMcp had no adapters option, and one given anyway was ignored without an error.
Every adapter needs a name, unique among the adapters of its class in the whole process: a second OpenapiAdapter.init({ name: "pets" }) throws Duplicate adapter name 'pets' for OpenapiAdapter, even for another server. See the OpenAPI adapter.
What a plugin can add
| A plugin can add | The official plugins' examples |
|---|---|
| Hooks that run on every call | The cache answers from its store before execute(); the approval plugin stops unapproved calls. |
Fields on @Tool and other decorators | cache (cache), approval (approval), codecall (CodeCall), and featureFlag on tools, resources, resource templates, prompts, skills and agents (feature flags). TypeScript knows about a field once your code imports the package. A server where an entry has approval or featureFlag and no plugin enforces it doesn't start: see Fields that need their plugin. |
Members on this | this.remember (Remember), this.approval (approval), this.featureFlags (feature flags). |
| Tools | CodeCall adds its codecall:* tools, and Remember its four memory tools when tools.enabled is set. |
| Providers | Services and stores you can get with this.get(), like Remember's RememberAccessorToken or CodeCall's AuditLoggerService. |
| A whole app | The dashboard's DashboardApp, with its own MCP endpoint. |
A field like cache goes at the top level of @Tool, next to name, not inside another object.
Fields that need their plugin
approval and featureFlag ask for protection, so a server whose entries use them without a plugin to enforce them refuses to start, with UnenforcedMetadataError and one line per entry: Unenforced metadata: Tool "bulk_export" declares 'featureFlag' (enforced by FeatureFlagPlugin from @frontmcp/plugin-feature-flags). …. Install the plugin on the server or on an app, or remove the field; a tool declared inside an @Agent needs the plugin on that agent. cache and codecall aren't checked: without their plugin they do nothing. A plugin you write can have fields checked the same way, with @Plugin({ enforcesMetadata }): see enforcing an option of your own.
ES module projects
The packages for Node servers load and run in an ES module project ("type": "module" in package.json) as in the CommonJS project frontmcp create makes. This was checked with Node 24, outside the Playground: each package above but WebMCP, which is for servers in a web page, loaded, and a server ran with the cache, CodeCall, dashboard and Remember plugins. The Split, LaunchDarkly and Unleash adapters and Remember's "vercel-kv" store load their SDKs as in a CommonJS project, and say so when one isn't installed.
Changed in 1.9: the ES module builds of the cache, CodeCall and dashboard plugins and of @frontmcp/plugins called require() without defining it, and failed when they were imported, with Dynamic require of "events" is not supported. Remember's "vercel-kv" store failed at init(), and the Split, LaunchDarkly and Unleash adapters said their SDK wasn't installed even when it was. A server bundled with a createRequire banner to get round this can drop the banner.
The dashboard
@frontmcp/plugin-dashboard shows a server's apps, tools, resources and prompts in a browser. It runs on FrontMCP's Node HTTP server only (a createFetchHandler() serves just the server's own apps), so it isn't in the Playground; what follows was checked there. Register the plugin for its options, and add DashboardApp to apps, which is what serves it:
import { FrontMcp } from "@frontmcp/sdk";
import { DashboardApp, DashboardPlugin } from "@frontmcp/plugin-dashboard";
import { HelpDeskApp } from "./help-desk.app";
@FrontMcp({
info: { name: "help-desk", version: "1.0.0" },
apps: [HelpDeskApp, DashboardApp],
plugins: [DashboardPlugin.init({ auth: { enabled: true, token: process.env.DASHBOARD_TOKEN } })],
})
export default class Server {}GET /dashboardanswers with an HTML page that loads React, React Flow and dagre fromesm.sh, so the browser needs internet access. The page reads the inventory from the dashboard's MCP endpoint./dashboardis also an MCP endpoint of its own, with three tools,dashboard:graph,dashboard:list-toolsanddashboard:list-resources, which describe the whole server. They aren't listed at the main endpoint, andcreateDirect(),createFetchHandler()and the other entry points serve the server's own apps even whenDashboardAppcomes first inapps. The endpoint applies the server's own auth first, then the dashboard'sauth.
| Option | Default | Description |
|---|---|---|
enabled | On, unless NODE_ENV is production | Serve the dashboard. When it's off, the page and the MCP endpoint both answer 404, the endpoint with The FrontMCP dashboard is disabled. To use the dashboard in production, set enabled: true with auth. |
basePath | "/dashboard" | Where the page is served. The MCP endpoint stays at /dashboard, and the page connects to it there. |
auth.enabled, auth.token | Off | Require the token for the page and the MCP endpoint, and answer 401 without it. enabled without token makes init() throw dashboard auth.enabled requires auth.token to be set. |
cdn | esm.sh URLs | Where the page loads its libraries from: entrypoint, react, reactDom, reactDomClient, reactJsxRuntime, reactRouter, xyflow, dagre, xyflowCss. |
With auth on, the token goes:
- To the page as
Authorization: Bearer <token>or?token=<token>. The page then sets a cookie,frontmcp_dashboard(HttpOnly,SameSite=Strict, andSecureover https), which its own MCP client sends. - To the MCP endpoint as
Authorization: Bearer <token>, asx-frontmcp-dashboard-token: <token>, or with that cookie.?token=isn't accepted there. On a server with its own auth,Authorizationcarries the server's credential, so send the dashboard token inx-frontmcp-dashboard-token. Without a valid token, the endpoint answers401withWWW-Authenticate: Bearer realm="frontmcp-dashboard".
DashboardPlugin.init() alone serves nothing: DashboardApp serves the dashboard, with the default options when there's no plugin. Dashboard options apply to the whole process: a second DashboardPlugin.init() with a different auth throws, and one with other differences replaces the first for every server, with a warning.
Writing your own
A plugin is a class with @Plugin that extends DynamicPlugin. @Plugin covers every option, hooks, providers, context members like this.remember, fields the server checks with enforcesMetadata, and where to register it, and Extending with Plugins builds one step by step. Hooks lists what a plugin can hook.
An adapter, which turns another description of an API into tools as the OpenAPI adapter does, is a class with @Adapter that extends DynamicAdapter: see @Adapter.
Usage
Using official plugins and an adapter together
A shop server: the catalog app caches its product lookups, every app can use this.remember because the Remember plugin is on the server, and the pets app's tools come from an OpenAPI spec:
import { FrontMcp } from "@frontmcp/sdk";
import { RememberPlugin } from "@frontmcp/plugin-remember";
import { CatalogApp, PetsApp } from "./apps";
@FrontMcp({
info: { name: "shop", version: "1.0.0" },
apps: [CatalogApp, PetsApp],
plugins: [RememberPlugin.init({ type: "memory" })], // this.remember, for every app
})
export default class Server {}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
Each plugin's own page has examples for its options. The adapter's tools call the API over HTTP; the OpenAPI adapter page shows calls, with the API stood in for.
Troubleshooting
Dynamic require of "events" is not supported
Your project is an ES module ("type": "module"), and its @frontmcp/* packages are older than 1.9.0. Update every one of them to 1.9.0 or later. See ES module projects.
Unenforced metadata: Tool "…" declares 'approval' (or 'featureFlag')
An entry has approval or featureFlag, and the plugin that enforces it isn't installed where it reaches the entry, so the server doesn't start. See Fields that need their plugin.
The dashboard answers 404 or 401
404 with The FrontMCP dashboard is disabled.: enabled is false, or NODE_ENV is production and enabled isn't set. 401: auth is on and the request has no valid token; at the MCP endpoint, ?token= doesn't count. See The dashboard.
RememberPlugin is not installed, or Provider "…" is not available, in another app's tool
The plugin is registered on one app, and the tool is in another. A plugin's providers, like this.remember or this.approval, reach only the app that registered it. Register the plugin on @FrontMcp, or on that app too. See Registering a plugin.
TypeError: (extraProviders ?? []) is not iterable when the server starts
A plugin's init() got a providers option that isn't an array, on a FrontMCP older than 1.9.0. Update to 1.9.0 or later, or call the option something else. See Registering a plugin.
An adapter's tools don't show up
On a FrontMCP older than 1.9.0, @FrontMcp ignored adapters. Update to 1.9.0 or later, or move the adapter to an @App. See Registering an adapter.
Duplicate adapter name 'x' for OpenapiAdapter
Two OpenapiAdapter.init() calls in one process used the same name, or a module that calls it was loaded twice. Give each adapter its own name. See the OpenAPI adapter.
Plugin "…" requires global "redis" configuration. Add "redis" to your @FrontMcp decorator options.
A plugin was given type: "global-store", which uses the server's store, and @FrontMcp has no redis. See the cache plugin's stores.
plugins items must be annotated with @Plugin()
Something in plugins isn't a plugin: an adapter, which goes in adapters, or a tool or provider. See @Plugin.