Built-in Plugins and Adapters
In Extending with Plugins you wrote a plugin that records every change in an audit log. Some jobs come up on almost every server, and for those the FrontMCP team publishes plugins of its own: answering a repeated lookup without running it again, remembering a user's preference, making sure a person agreed before something is deleted, shipping a tool to some users before the rest. There's also CodeCall, for servers with more tools than a model can read, and an adapter that turns an OpenAPI description of a REST API into tools. This chapter teaches each one with the help desk, and points out where FrontMCP 1.9.2 behaves differently from what an option's name suggests.
In this chapter
- How to answer repeated calls from a cache
- How tools remember values from one call to the next
- How to require a person's approval before a tool runs
- How to turn tools on and off with feature flags
- How to let the model find tools and call them from a short script
- How to turn an OpenAPI spec into tools
Using an official plugin
Each is its own npm package. The plugins are registered like the one you wrote, with SomePlugin.init({ ... }) in the plugins of an @App, for that app's tools, or of @FrontMcp, for every app's, and each adds something your code opts in to. What a plugin adds to this, like this.remember, reaches only the tools of the apps it's registered for:
| Package | Adds | Lesson |
|---|---|---|
@frontmcp/plugin-cache | cache on @Tool | Caching Results |
@frontmcp/plugin-remember | this.remember, and four memory tools you can turn on | Remembering Across Calls |
@frontmcp/plugin-approval | approval on @Tool, and this.approval | Asking for Approval |
@frontmcp/plugin-feature-flags | featureFlag on tools, resources, prompts, skills and agents, and this.featureFlags | Turning Features On and Off |
@frontmcp/plugin-codecall | Six codecall:* tools in front of yours | Letting the Model Write Code with CodeCall |
@frontmcp/adapters | OpenapiAdapter: a tool for each operation in a spec | Wrapping an OpenAPI Service |
A few rules hold for all of them:
- Register a plugin with
init(). With no argument it's the same asinit({}), the defaults, for the plugins whose options all have defaults. Feature flags need anadapter:FeatureFlagPlugin.init()throws aFeatureFlagConfigurationErrorwithout it. The bare class,plugins: [SomePlugin], is the same asinit()with no argument, soplugins: [FeatureFlagPlugin]stops the server with that error. Before 1.9.4 the bare class worked for the cache plugin only. approvalandfeatureFlagneed their plugin. A server with a tool that declares one of them, and no plugin to enforce it, refuses to start, so a gate can't be left off by accident.cacheandcodecallwithout their plugin do nothing.- Adapters go in
adapters, not inplugins: an app's, for that app, or@FrontMcp's, whose adapter tools every app serves. - Every package loads in CommonJS and ES module projects alike. Before 1.9, an ES module project (
"type": "module") couldn't import the cache and CodeCall plugins: see ES module projects.
Caching results
A lookup the model repeats, like a customer's account from a slow CRM, doesn't need to run twice. With the cache plugin on the app and cache: { ttl } on the tool, the next call with the same arguments is answered from a store, without running execute():
import { App, Tool, ToolContext, z } from "@frontmcp/sdk";
import { CachePlugin } from "@frontmcp/plugin-cache";
import { crm } from "./crm";
@Tool({
name: "get_customer",
description: "Get a customer account by its id. May be up to 5 minutes old.",
inputSchema: { id: z.string().describe("Customer id, like C-1") },
annotations: { readOnlyHint: true },
cache: { ttl: 300 }, // seconds
})
export class GetCustomer extends ToolContext {
async execute({ id }: { id: string }) {
return crm.getCustomer(id);
}
}
@App({
id: "help-desk",
name: "Help Desk",
tools: [GetCustomer],
plugins: [CachePlugin.init({ type: "memory", keyByIdentity: false })], // the same answer for every caller
})
export class HelpDeskApp {}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
By default each caller has entries of their own. That keeps one user's answer from reaching another, and it also means an anonymous caller under MCP 2026-07-28, who is new on every request, is never answered from the cache. keyByIdentity: false shares entries, and is only safe for answers that are the same for everyone.
Ready to learn this topic?
Learn how to turn the cache on, why it answers nothing for anonymous callers, how to keep per-caller answers apart and share the rest, what makes two calls the same, how long an entry lives and why it can't be cleared, and what not to cache.
Read Caching ResultsRemembering across calls
A tool forgets everything when its call ends. The Remember plugin gives every tool this.remember, a key-value memory whose scope decides who reads a value back. user scope keeps a value for the caller. The tests call the server in-process with createDirect() as two signed-in agents, and as the Playground's anonymous client, which user scope refuses:
import { App, Tool, ToolContext, z } from "@frontmcp/sdk";
import { RememberPlugin } from "@frontmcp/plugin-remember";
@Tool({ name: "set_signature", description: "Set the signature your replies end with.", inputSchema: { signature: z.string() } })
export class SetSignature extends ToolContext {
async execute({ signature }: { signature: string }) {
await this.remember.set("signature", signature, { scope: "user" });
return { signature };
}
}
@Tool({ name: "draft_reply", description: "Draft a reply, signed with your signature.", inputSchema: { ticketId: z.string(), text: z.string() } })
export class DraftReply extends ToolContext {
async execute({ ticketId, text }: { ticketId: string; text: string }) {
const signature = await this.remember.get("signature", { scope: "user", defaultValue: "The Help Desk" });
return { ticketId, reply: `${text}\n\n${signature}` };
}
}
@App({ id: "help-desk", name: "Help Desk", tools: [SetSignature, DraftReply], plugins: [RememberPlugin.init({ type: "memory" })] })
export class HelpDeskApp {}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
The other scopes matter as much: global is shared by everyone, and session, the default, doesn't mean a conversation under MCP 2026-07-28. It lasts for the signed-in caller. An anonymous caller can't use user or session: Remember refuses it with REMEMBER_IDENTITY_REQUIRED, and only global works for it.
Ready to learn this topic?
Learn how to store and read values with this.remember, what each scope keeps for signed-in and anonymous callers under MCP 2026-07-28, how to make values expire and forget them, and how to give the model memory tools of its own, limited to the scopes you choose.
Asking for approval
destructiveHint tells a client a tool can't be undone, and the client may still run it without asking anyone. The approval plugin refuses every call to a tool with approval until an approval of it is recorded for the caller:
import { App, Tool, ToolContext, z } from "@frontmcp/sdk";
import { ApprovalPlugin } from "@frontmcp/plugin-approval";
export const tickets = new Map([
["T-1", { id: "T-1", title: "Cannot log in" }],
["T-2", { id: "T-2", title: "Invoice total is wrong" }],
]);
@Tool({
name: "delete_ticket",
description: "Delete a support ticket for good. Needs the user's approval: call approve_delete first.",
inputSchema: { id: z.string().describe("Ticket id, like T-1") },
annotations: { destructiveHint: true },
approval: { approvalMessage: "delete_ticket needs the user's approval. Call approve_delete first." },
})
export class DeleteTicket extends ToolContext {
async execute({ id }: { id: string }) {
return { id, deleted: tickets.delete(id) };
}
}
@App({ id: "help-desk", name: "Help Desk", tools: [DeleteTicket], plugins: [ApprovalPlugin.init({ storage: { type: "memory" } })] })
export class HelpDeskApp {}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
The plugin doesn't ask anyone itself. Your code records an approval, with this.approval, once the user says yes in a form from this.elicit(), which under MCP 2026-07-28 the model can't answer. An approval belongs to a signed-in caller, so an anonymous caller can never keep one.
Ready to learn this topic?
Learn how to put a tool behind approval, what the model sees when a call is refused, which is the same in production, how to record an approval only after the user says yes, and how to withdraw one, limit how long it lasts, and why a session approval lasts for the user under MCP 2026-07-28.
Read Asking for ApprovalTurning features on and off
A feature flag ships a tool to some users before the rest, or turns one off without a deploy. With the feature flags plugin, featureFlag: "bulk-export" on a tool hides it from every list while the flag is off for the caller, and refuses it if it's called anyway. Flags come from an adapter: fixed values, as here, or LaunchDarkly, Split, Unleash or your own. Open the Capabilities tab to see which tools are listed:
import { App, Tool, ToolContext } from "@frontmcp/sdk";
import { FeatureFlagPlugin } from "@frontmcp/plugin-feature-flags";
@Tool({ name: "search_tickets", description: "Search support tickets.", inputSchema: {} })
export class SearchTickets extends ToolContext {
async execute() {
return { tickets: ["T-1", "T-3"] };
}
}
@Tool({ name: "bulk_export", description: "Export every ticket as CSV.", inputSchema: {}, featureFlag: "bulk-export" })
export class BulkExport extends ToolContext {
async execute() {
return { csv: "id,title\nT-1,Cannot log in" };
}
}
@App({
id: "help-desk",
name: "Help Desk",
tools: [SearchTickets, BulkExport],
plugins: [FeatureFlagPlugin.init({ adapter: "static", flags: { "bulk-export": false } })],
})
export class HelpDeskApp {}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
Change "bulk-export": false to true, and bulk_export appears. Each caller's flags are evaluated for them, from their token, so a flag can be on for some users and off for the rest; under MCP 2026-07-28 every anonymous caller gets the same answer.
Ready to learn this topic?
Learn why a switch inside execute() is the wrong way to hide a tool, how to put tools, resources and prompts behind a flag, what a caller sees while it's off, how to make a flag a kill switch that's on by default, and how to turn a flag on for some callers first and read it inside a tool.
Letting the model write code with CodeCall
A server with dozens of tools sends every tool's schema to the model before it can pick one. CodeCall puts them behind six meta-tools instead: the model searches with codecall:search, reads the schemas of the tools it picked with codecall:describe, and calls them one at a time with codecall:invoke, or several at once from a short script with codecall:execute:
import { App } from "@frontmcp/sdk";
import { CodeCallPlugin } from "@frontmcp/plugin-codecall";
import { CloseTicket, GetCustomer, GetTicket, SearchTickets } from "./tools";
@App({
id: "help-desk",
name: "Help Desk",
tools: [SearchTickets, GetTicket, CloseTicket, GetCustomer],
plugins: [CodeCallPlugin.init({ mode: "codecall_only" })],
})
export class HelpDeskApp {}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
A script runs on the server, in a sandbox, and can loop over results and join them, so only the answer goes back to the model. This one lists the urgent open tickets with their customers, in one request:
const { tickets } = await callTool("search_tickets", { status: "open" });
const urgent = tickets.filter((t) => t.priority === "high");
const out = [];
for (const t of urgent) {
const customer = await callTool("get_customer", { id: t.customerId });
out.push({ ticket: t.id, customer: customer.name });
}
return out;
Here are the tools above again, with codecall:execute called with that script. The Playground runs it in a browser sandbox, where a server uses one built on Node's vm; the answer is the same:
Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
codecall:execute returns { "status": "ok", "result": [{ "ticket": "T-1", "customer": "Acme Corp" }, { "ticket": "T-3", "customer": "Globex" }] }.
Ready to learn this topic?
Learn what a long tool list costs a model, how to put your tools behind CodeCall and when it's worth it, how the model finds, reads and calls them, one at a time or from a script, what a script may do, and how to keep tools out of its reach.
Read Letting the Model Write Code with CodeCallWrapping an OpenAPI service
When what the model needs already exists as a REST API with an OpenAPI description, OpenapiAdapter from @frontmcp/adapters makes a tool of each operation, named by its operationId, which sends the request the spec describes and hands back the API's answer. The Playground can't reach a real API, so desk.example.ts plays it, in-process:
import "./desk.example";
import { App } from "@frontmcp/sdk";
import { OpenapiAdapter } from "@frontmcp/adapters";
import { spec } from "./spec";
@App({
id: "desk",
name: "Desk",
adapters: [OpenapiAdapter.init({ name: "desk", baseUrl: "https://desk.example/v1", spec })],
})
export class DeskApp {}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
The model gets the API's answer as it is, and every operation in the spec as a tool, which is a lot of tools for a large API. The adapter's options choose which operations become tools, rename and describe them, send the API's credentials, and reshape what comes back. The adapter goes in an app's adapters, and each one needs a name of its own.
Ready to learn this topic?
Learn how to turn an OpenAPI description into tools, what a call sends to the API and what the model gets back, how to give the adapter the API's credentials without the model seeing them, how to choose and name the operations the model sees, and what to do when an API is too big for one tool per operation.
Read Wrapping an OpenAPI ServicePlugin, adapter or your own code?
| If you need… | Use |
|---|---|
| The same answer again, without the wait | The cache plugin, on tools that only read |
| A value that outlasts one call | The Remember plugin, in the scope that matches who should read it |
| A person's yes before a tool runs | The approval plugin, with a form only the user can answer |
| A tool for some users, or one you can switch off | The feature flags plugin |
| More tools than a model can usefully read | CodeCall |
| An existing REST API as tools | The OpenAPI adapter, or a few tools with this.fetch() when the model only needs a few well-shaped calls |
| Something none of these do | A plugin of your own |
Every option of each one is in the plugins and adapters reference.
What's next?
Start the chapter with Caching Results. After it, Testing and Shipping covers proving your server works and getting it into production.