# Plugins and adapters

> The official FrontMCP plugins and the OpenAPI adapter, which package each one is in, how to install and register them, what they add to your server, and what to know before you use one, including ES module projects.

Source: https://frontmcp.dev/reference/plugins

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`](https://frontmcp.dev/reference/sdk/plugin) covers writing your own.

```ts
@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](https://frontmcp.dev/reference/plugins/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](https://frontmcp.dev/reference/plugins/remember) |
| `@frontmcp/plugin-approval` | `ApprovalPlugin` | Stops tools marked with `approval` until the call is approved. | [Approval](https://frontmcp.dev/reference/plugins/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](https://frontmcp.dev/reference/plugins/feature-flags) |
| `@frontmcp/plugin-codecall` | `CodeCallPlugin` | Six `codecall:*` tools with which the model searches your tools and runs short scripts that call them. | [CodeCall](https://frontmcp.dev/reference/plugins/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](https://frontmcp.dev/reference/plugins/skilled-openapi) |
| `@frontmcp/plugin-dashboard` | `DashboardPlugin`, `DashboardApp` | A web page and an MCP endpoint that show what the server has. | [The dashboard](#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](https://frontmcp.dev/reference/plugins/webmcp) |
| `@frontmcp/adapters` | `OpenapiAdapter` | A tool for each operation in an OpenAPI 3 spec, which calls the API. | [OpenAPI adapter](https://frontmcp.dev/reference/adapters/openapi) |

### 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:

```bash
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](https://frontmcp.dev/reference/sdk/plugin#where-you-register-a-plugin). An [`@Agent`](https://frontmcp.dev/reference/sdk/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](https://frontmcp.dev/reference/sdk/plugin#putting-a-plugin-on-an-agent), and [flagging an agent](https://frontmcp.dev/reference/plugins/feature-flags#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`](https://frontmcp.dev/reference/sdk/plugin#dynamicpluginoptions-input).
- **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](https://frontmcp.dev/reference/adapters/openapi).

### 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](#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](https://frontmcp.dev/reference/sdk/plugin#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()`](https://frontmcp.dev/reference/sdk/create-fetch-handler) 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:

```ts 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()`](https://frontmcp.dev/reference/sdk/create#frontmcpinstancecreatedirectconfig), `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](https://frontmcp.dev/reference/auth/modes) first, then the dashboard's `auth`.

| 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`, 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`](https://frontmcp.dev/reference/sdk/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](https://frontmcp.dev/learn/extending-with-plugins) builds one step by step. [Hooks](https://frontmcp.dev/reference/sdk/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`](https://frontmcp.dev/reference/sdk/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:

```ts main.ts active
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 {}
```

```ts apps.ts
import { App, Tool, ToolContext, z } from "@frontmcp/sdk";
import { CachePlugin } from "@frontmcp/plugin-cache";
import { OpenapiAdapter } from "@frontmcp/adapters";
import { petsSpec } from "./pets.openapi";

let lookups = 0;

@Tool({
  name: "get_product",
  description: "A product by SKU",
  inputSchema: { sku: z.string() },
  cache: { ttl: 600 }, // a field the cache plugin adds to @Tool
})
export class GetProduct extends ToolContext {
  async execute({ sku }: { sku: string }) {
    return { sku, name: "Mechanical keyboard", lookup: ++lookups };
  }
}

@Tool({ name: "feature_product", description: "Feature a product on the home page", inputSchema: { sku: z.string() } })
export class FeatureProduct extends ToolContext {
  async execute({ sku }: { sku: string }) {
    await this.remember.set("featured", sku, { scope: "global" }); // a member the Remember plugin adds
    return { featured: sku };
  }
}

@Tool({ name: "featured_product", description: "The product on the home page", inputSchema: {} })
export class FeaturedProduct extends ToolContext {
  async execute() {
    return { featured: await this.remember.get("featured", { scope: "global", defaultValue: null }) };
  }
}

@App({
  id: "catalog",
  name: "Catalog",
  tools: [GetProduct, FeatureProduct, FeaturedProduct],
  plugins: [CachePlugin.init({ type: "memory", keyByIdentity: false })], // the same answer for everyone
})
export class CatalogApp {}

@App({
  id: "pets",
  name: "Pets",
  adapters: [OpenapiAdapter.init({ name: "pets", baseUrl: "https://pets.example.com", spec: petsSpec })],
})
export class PetsApp {}
```

```ts pets.openapi.ts
export const petsSpec = {
  openapi: "3.0.3",
  info: { title: "Pets", version: "1.0.0" },
  paths: {
    "/pets/{id}": {
      get: {
        operationId: "getPet",
        summary: "Get a pet by id",
        parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
        responses: { "200": { description: "The pet" } },
      },
    },
  },
};
```

```ts plugins.test.ts
import { test, expect } from "@frontmcp/testing";
import { App, DynamicPlugin, FrontMcpInstance, Plugin, Tool, ToolContext } from "@frontmcp/sdk";
import { CachePlugin } from "@frontmcp/plugin-cache";
import { RememberPlugin } from "@frontmcp/plugin-remember";
import { ApprovalPlugin } from "@frontmcp/plugin-approval";
import { CodeCallPlugin } from "@frontmcp/plugin-codecall";
import { FeatureFlagConfigurationError, FeatureFlagPlugin } from "@frontmcp/plugin-feature-flags";
import { SkilledOpenApiPlugin } from "@frontmcp/plugin-skilled-openapi";
import { OpenapiAdapter } from "@frontmcp/adapters";
import * as umbrella from "@frontmcp/plugins";
import { CatalogApp, FeaturedProduct, FeatureProduct, PetsApp } from "./apps";
import { petsSpec } from "./pets.openapi";

test("the adapter turns each operation into a tool", async ({ mcp }) => {
  expect(await mcp.tools.list()).toContainTool("getPet");
});

test("`cache` on @Tool comes from the cache plugin", async ({ mcp }) => {
  const first = await mcp.tools.call("get_product", { sku: "MS-7" });
  const second = await mcp.tools.call("get_product", { sku: "MS-7" });
  expect(second.json().lookup).toBe(first.json().lookup);
  expect(second.raw._meta.cache).toBe("hit");
});

test("`this.remember` comes from the Remember plugin on the server", async ({ mcp }) => {
  await mcp.tools.call("feature_product", { sku: "KB-101" });
  expect((await mcp.tools.call("featured_product", {})).json()).toEqual({ featured: "KB-101" });
});

test("@frontmcp/plugins exports the same classes", () => {
  expect(umbrella.CachePlugin).toBe(CachePlugin);
  expect(umbrella.RememberPlugin).toBe(RememberPlugin);
});

test("`init()` with no argument is the same as `init({})`", () => {
  for (const plugin of [CachePlugin, RememberPlugin, ApprovalPlugin, CodeCallPlugin]) {
    expect(() => plugin.init()).not.toThrow();
  }
});

test("a plugin with a required option still needs it", () => {
  expect(() => SkilledOpenApiPlugin.init()).toThrow(/source/);
  expect(() => FeatureFlagPlugin.init()).toThrow(FeatureFlagConfigurationError);
});

test("the bare class is the same as `init()` with no argument", async () => {
  const info = { name: "shop", version: "1.0.0" };

  @App({ id: "catalog", name: "Catalog", tools: [FeatureProduct, FeaturedProduct], plugins: [RememberPlugin] })
  class BareRemember {}

  const server = await FrontMcpInstance.createDirect({ info, apps: [BareRemember] });
  await server.callTool("feature_product", { sku: "KB-101" });
  expect((await server.callTool("featured_product", {})).structuredContent).toEqual({ featured: "KB-101" });
  await server.dispose();

  @App({ id: "flags", name: "Flags", tools: [FeatureProduct], plugins: [FeatureFlagPlugin] })
  class BareFeatureFlags {}

  await expect(FrontMcpInstance.createDirect({ info, apps: [BareFeatureFlags] })).rejects.toThrow(FeatureFlagConfigurationError);
});

test("a plugin on one app doesn't give `this.remember` to the tools of another", async () => {
  @Tool({ name: "read_featured", description: "The product on the home page", inputSchema: {} })
  class ReadFeatured extends ToolContext {
    async execute() {
      return { featured: await this.remember.get("featured", { scope: "global", defaultValue: null }) };
    }
  }

  @App({ id: "writer", name: "Writer", tools: [FeatureProduct], plugins: [RememberPlugin.init({ type: "memory" })] })
  class WriterApp {}

  @App({ id: "reader", name: "Reader", tools: [ReadFeatured] })
  class ReaderApp {}

  const server = await FrontMcpInstance.createDirect({ info: { name: "shop", version: "1.0.0" }, apps: [WriterApp, ReaderApp] });
  await server.callTool("feature_product", { sku: "KB-101" });
  await expect(server.callTool("read_featured", {})).rejects.toThrow("RememberPlugin is not installed. Add RememberPlugin.init() to your plugins array.");
  await server.dispose();
});

test("an option named like a list stays an option, and dynamicTools turns it into tools", async () => {
  @Tool({ name: "audit_trail", description: "The audit trail", inputSchema: {} })
  class AuditTrail extends ToolContext {
    async execute() {
      return { entries: [] };
    }
  }

  @Plugin({ name: "audit" })
  class AuditPlugin extends DynamicPlugin<{ tools?: { enabled?: boolean } }> {
    static dynamicTools = (options: { tools?: { enabled?: boolean } }) => (options.tools?.enabled ? [AuditTrail] : []);
  }

  const listed = async (options: object) => {
    @App({ id: "audit", name: "Audit", plugins: [AuditPlugin.init(options)] })
    class Audited {}
    const server = await FrontMcpInstance.createDirect({ info: { name: "shop", version: "1.0.0" }, apps: [Audited] });
    const { tools } = await server.listTools();
    await server.dispose();
    return tools.map((t: { name: string }) => t.name);
  };

  expect(await listed({ tools: { enabled: true } })).toEqual(["audit_trail"]);
  expect(await listed({ tools: { enabled: false } })).toEqual([]);
  expect(await listed({ tools: [AuditTrail] })).toEqual(["audit_trail"]); // an array registers what it lists
});

test("so does an option named providers that isn't an array", async () => {
  type AuditOptions = { providers?: { retention: string } };

  @Plugin({ name: "audit" })
  class AuditPlugin extends DynamicPlugin<AuditOptions> {
    static dynamicTools = (options: AuditOptions) => {
      @Tool({ name: "audit_retention", description: "How long the audit trail is kept", inputSchema: {} })
      class AuditRetention extends ToolContext {
        async execute() {
          return { retention: options.providers?.retention };
        }
      }
      return [AuditRetention];
    };
  }

  @App({ id: "audit", name: "Audit", plugins: [AuditPlugin.init({ providers: { retention: "30d" } })] })
  class Audited {}
  const server = await FrontMcpInstance.createDirect({ info: { name: "shop", version: "1.0.0" }, apps: [Audited] });
  expect((await server.callTool("audit_retention", {})).structuredContent).toEqual({ retention: "30d" });
  await server.dispose();
});

test("`featureFlag` without its plugin stops the server", async () => {
  @Tool({ name: "bulk_export", description: "Export every product as CSV", inputSchema: {}, featureFlag: "bulk-export" })
  class BulkExport extends ToolContext {
    async execute() {
      return { csv: "sku,name" };
    }
  }
  const withPlugins = (...plugins: object[]) => {
    @App({ id: "catalog", name: "Catalog", tools: [BulkExport], plugins: plugins as never })
    class Catalog {}
    return FrontMcpInstance.createDirect({ info: { name: "shop", version: "1.0.0" }, apps: [Catalog] });
  };

  await expect(withPlugins()).rejects.toThrow(
    "Tool \"bulk_export\" declares 'featureFlag' (enforced by FeatureFlagPlugin from @frontmcp/plugin-feature-flags)",
  );
  const server = await withPlugins(FeatureFlagPlugin.init({ adapter: "static", flags: { "bulk-export": true } }));
  await server.dispose();
});

test("`cache` and `codecall` without their plugin do nothing", async () => {
  let runs = 0;
  @Tool({ name: "get_price", description: "A product's price", inputSchema: {}, cache: true, codecall: { enabledInCodeCall: true } })
  class GetPrice extends ToolContext {
    async execute() {
      return { run: ++runs };
    }
  }
  @App({ id: "prices", name: "Prices", tools: [GetPrice] })
  class Prices {}

  const server = await FrontMcpInstance.createDirect({ info: { name: "shop", version: "1.0.0" }, apps: [Prices] });
  await server.callTool("get_price", {});
  expect((await server.callTool("get_price", {})).structuredContent).toEqual({ run: 2 }); // not cached
  await server.dispose();
});

test("an adapter in @FrontMcp serves its tools from every app", async () => {
  const server = await FrontMcpInstance.createDirect({
    info: { name: "shop", version: "1.0.0" },
    apps: [CatalogApp],
    adapters: [OpenapiAdapter.init({ name: "pets-on-server", baseUrl: "https://pets.example.com", spec: petsSpec })],
  });
  const { tools } = await server.listTools();
  expect(tools.map((t: { name: string }) => t.name)).toContain("getPet");
  await server.dispose();
});

test("two adapters that make the same tool name each put their name in front", async () => {
  const server = await FrontMcpInstance.createDirect({
    info: { name: "shop", version: "1.0.0" },
    apps: [CatalogApp, PetsApp],
    adapters: [OpenapiAdapter.init({ name: "pets-v2", baseUrl: "https://pets.example.com", spec: petsSpec })],
  });
  const { tools } = await server.listTools();
  expect(tools.map((t: { name: string }) => t.name).filter((name: string) => name.endsWith("getPet")).sort()).toEqual(["pets-v2:getPet", "pets:getPet"]);
  await server.dispose();
});
```

Each plugin's own page has examples for its options. The adapter's tools call the API over HTTP; the [OpenAPI adapter](https://frontmcp.dev/reference/adapters/openapi) 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](#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](#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](#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](#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](#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](#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](https://frontmcp.dev/reference/adapters/openapi).

### `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](https://frontmcp.dev/reference/plugins/cache#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`](https://frontmcp.dev/reference/sdk/plugin#app-invalid-metadata-for-plugins-plugins-items-must-be-annotated-with-plugin).
