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

PackageExportWhat it doesPage
@frontmcp/plugin-cacheCachePluginAnswers repeated tool calls from a store instead of running the tool again.Cache
@frontmcp/plugin-rememberRememberPluginthis.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-approvalApprovalPluginStops tools marked with approval until the call is approved.Approval
@frontmcp/plugin-feature-flagsFeatureFlagPluginGates tools, resources, prompts and skills behind flags from a static list, Split, LaunchDarkly, Unleash or your own source.Feature flags
@frontmcp/plugin-codecallCodeCallPluginSix codecall:* tools with which the model searches your tools and runs short scripts that call them.CodeCall
@frontmcp/plugin-skilled-openapiSkilledOpenApiPluginServes a REST API as skill bundles, with its operations behind a few meta-tools instead of one tool each.Skilled OpenAPI
@frontmcp/plugin-dashboardDashboardPlugin, DashboardAppA web page and an MCP endpoint that show what the server has.The dashboard
@frontmcp/plugin-webmcpWebMcpPluginOffers the tools of a server that runs in a web page to the agents in the user's browser, through WebMCP.WebMCP
@frontmcp/adaptersOpenapiAdapterA 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 checkThat 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'sAdded to that appAdded once, for the server
Providers the plugin builds from its options, like the one behind this.rememberThat app's toolsEvery 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() and CodeCallPlugin.init() don't throw: each is the same as init({}). A required option is still required: feature flags need an adapter (FeatureFlagPlugin.init() throws a FeatureFlagConfigurationError), and Skilled OpenAPI needs a source (init() throws a ZodError). The bare class, plugins: [RememberPlugin], is the same as init() 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 its init() throws. Changed in 1.9.4: only the cache plugin worked as a bare class. Remember had no this.remember, the approval and feature flags plugins failed with Provider "…" is not available once they were used, and CodeCall left the server with no tools.
  • init() runs when your module is imported, not when the server starts. See DynamicPlugin.
  • An option named like a list field of @Plugin is that field only when it's an array. init() copies its options onto the plugin's registration, where tools, resources, prompts, skills, adapters, plugins, exports and providers describe the plugin itself. An array registers what it lists. Any other value stays the plugin's option, which is how Remember takes tools: { enabled: true }, and a plugin can add tools from it with a static dynamicTools(options), as dynamicProviders(options) adds providers. The Playground below runs each case. Changed in 1.9: an option named providers that wasn't an array failed init() with TypeError: (extraProviders ?? []) is not iterable.
  • Options named name, id, description or scope are 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, so init({ name: "eu" }) renamed the plugin and init({ 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 addThe official plugins' examples
Hooks that run on every callThe cache answers from its store before execute(); the approval plugin stops unapproved calls.
Fields on @Tool and other decoratorscache (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 thisthis.remember (Remember), this.approval (approval), this.featureFlags (feature flags).
ToolsCodeCall adds its codecall:* tools, and Remember its four memory tools when tools.enabled is set.
ProvidersServices and stores you can get with this.get(), like Remember's RememberAccessorToken or CodeCall's AuditLoggerService.
A whole appThe 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:

main.ts
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 /dashboard answers with an HTML page that loads React, React Flow and dagre from esm.sh, so the browser needs internet access. The page reads the inventory from the dashboard's MCP endpoint.
  • /dashboard is also an MCP endpoint of its own, with three tools, dashboard:graph, dashboard:list-tools and dashboard:list-resources, which describe the whole server. They aren't listed at the main endpoint, and createDirect(), createFetchHandler() and the other entry points serve the server's own apps even when DashboardApp comes first in apps. The endpoint applies the server's own auth first, then the dashboard's auth.
OptionDefaultDescription
enabledOn, unless NODE_ENV is productionServe 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.tokenOffRequire 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.
cdnesm.sh URLsWhere 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, and Secure over https), which its own MCP client sends.
  • To the MCP endpoint as Authorization: Bearer <token>, as x-frontmcp-dashboard-token: <token>, or with that cookie. ?token= isn't accepted there. On a server with its own auth, Authorization carries the server's credential, so send the dashboard token in x-frontmcp-dashboard-token. Without a valid token, the endpoint answers 401 with WWW-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:

Open
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.