Skip to main content
The Feature Flags Plugin gates tools, resources, prompts, and skills based on feature flag evaluation — enabling progressive rollouts, A/B testing, and dynamic capability management for FrontMCP servers.

Why Use Feature Flags?

Declarative Gating

Annotate capabilities with featureFlag metadata — no conditional logic in your tool code

Multiple Providers

Split.io, LaunchDarkly, Unleash, static flags, or your own custom adapter

Per-User Targeting

Evaluate flags per user or session via auth context for targeted rollouts

Execution Gate

Blocks direct tools/call, resources/read, prompts/get, completion/complete and skill loads that bypass the list filter — no sneaking past disabled flags

Installation

How It Works

1

Hook Registration

The plugin registers hooks on the list flows for tools, resources, resource templates and prompts, and on the skills:filter flow that every skill surface runs
2

Metadata Collection

When capabilities are listed, the plugin collects featureFlag refs from each entry’s metadata
3

Batch Evaluation

Flags are batch-evaluated via the configured adapter in a single call
4

Capability Filtering

Capabilities with disabled flags are filtered out before reaching the client. Execution gates also block direct tools/call, resources/read, prompts/get and completion/complete for disabled flags, and a disabled skill is not found when loaded or read, so a flag withholds the capability rather than merely hiding it from the menu.

Quick Start

Basic Setup

Annotating a Tool

Programmatic Flag Checks

The plugin extends all execution contexts with this.featureFlags:

Adapters

Static

Best for: Development, testing, and demos with fixed flag values.

Split.io

Requires the @splitsoftware/splitio peer dependency:

LaunchDarkly

Requires the @launchdarkly/node-server-sdk peer dependency:

Unleash

Requires the unleash-client peer dependency:

Custom

Provide your own adapter implementing the FeatureFlagAdapter interface:
The FeatureFlagAdapter interface:

Annotating Capabilities

Add featureFlag to any tool, resource, resource template, prompt, or skill metadata. Capabilities with disabled flags are hidden from list responses and refused on direct access. The refusal is a public FeatureFlagDisabledError (code FEATURE_FLAG_DISABLED, HTTP 403) whose message names the capability and the flag, in every environment. A client that cached an earlier listing, or holds a URI from a previous session, gets an error rather than the capability. When the plugin is installed on an app (@App({ plugins: [FeatureFlagPlugin.init(...)] })), the gates cover every capability the app provides, including those its adapters (for example an OpenAPI adapter) and its plugins contribute. Its tool, resource, prompt and completion gates also cover the flagged capabilities of apps with no feature-flag plugin of their own, but not those of an app that installs its own; install the plugin on @FrontMcp({ plugins }) to gate every app with one adapter. Listings follow the same plugin as the gates: each capability and skill is hidden or listed by the copy of the plugin that judges it when it is called or read, so with a plugin on each of two apps, each app’s flags decide for that app’s entries only. Up to 1.8.3 every copy filtered every app’s listings, so one app’s flags could hide another app’s capability that its own plugin still served. Releases up to 1.8.2 hid another app’s flagged-off tool from tools/list but still ran it when called by name. Resources and prompts the server serves outside every app, such as the SEP-2640 skill:// resources, are gated by every installed copy of the plugin. @Agent({ featureFlag }) hides and refuses the agent’s invoke_<agent> tool like a tool’s flag. A server where a capability or agent declares featureFlag and no FeatureFlagPlugin reaches it refuses to start with UnenforcedMetadataError, naming the entries. This includes a server with no FeatureFlagPlugin at all, a splitByApp app whose own scope has none, and a tool declared inside an @Agent, which only a plugin installed on that agent gates. Up to 1.8.2 such a server started and served the flagged-off capabilities, and @Agent({ featureFlag }) was ignored. The toolName completion of ui://widget/{toolName}.html offers only the UI tools tools/list shows the caller: the SDK runs the caller’s tools:list-tools flow for it, so the list filter hides a flagged-off tool there too (#596). Skill surfaces evaluate flags for the calling user on every transport, stdio and in-memory clients included, so userIdResolver and attributesResolver see the caller rather than an anonymous session. The skill catalog the server embeds in its initialize instructions (and the SEP-2640 skill:// hints, when skillsConfig.sep2640InInstructions is on) is filtered the same way, for the client that initializes: a flag-disabled skill’s name and description are left out (#603). A skill’s skill://<path>/SKILL.md entry in resources/list is gated by the skill that path serves now. When a skill is replaced at the same path, for example by a dynamic registration, the entry takes the new skill’s flag (#606).

String Shorthand

Object Form

Use the object form to specify a defaultValue — the fallback when the adapter throws or the flag is unknown:

FeatureFlagRef Fields

When using the string shorthand (featureFlag: 'key'), defaultValue is false.

Plugin Options

'static' | 'splitio' | 'launchdarkly' | 'unleash' | 'custom'
required
The feature flag provider to use. FeatureFlagPlugin.init() with no adapter, or an unknown one, throws a FeatureFlagConfigurationError at startup that names the option and the supported adapters (it used to start and answer every request with a 500).
Record<string, boolean | FeatureFlagVariant>
Static flag values (only used with adapter: 'static')
object
Provider-specific configuration (varies by adapter):
  • Split.io: { apiKey: string }
  • LaunchDarkly: { sdkKey: string }
  • Unleash: { url: string, appName: string, apiKey?: string }
FeatureFlagAdapter
Custom adapter instance (only used with adapter: 'custom')
boolean
default:"false"
Global fallback when the adapter throws an error during evaluation
'session' | 'request' | 'none'
default:"'none'"
How to cache flag evaluation results:
  • session — cache per session, expires after cacheTtlMs
  • request — cache per request lifecycle
  • none — no caching, evaluate every time
number
default:"30000"
Cache TTL in milliseconds (used with cacheStrategy: 'session')
(ctx: FrontMcpContext) => string | undefined
Custom function to extract the user ID from the request context. By default, the plugin reads authInfo.extra.sub, authInfo.extra.userId, or authInfo.clientId, and passes none for an anonymous caller (an anon: subject, which the server makes up for each request of a caller without a session).
(ctx: FrontMcpContext) => Record<string, unknown>
Custom function to extract targeting attributes from the request context. Attributes are passed to the adapter for per-user targeting rules.
The evaluation context’s sessionId is the session the server verified (authInfo.sessionId). A caller without one gets none, so session-targeted rules fall back to the user or to anonymous. This includes an MCP 2026-07-28 client, which has no session: neither the mcp-session-id it sends nor the id the server makes up for each of its requests is one. Releases up to 1.8.2 passed whatever mcp-session-id the client sent, so a client could pick its own bucket. For the same reason, don’t build identity in these resolvers from ctx.sessionId or from request headers.

API Reference

FeatureFlagAccessor Methods

Access via this.featureFlags in any execution context (ToolContext, AgentContext, etc.).
Promise<boolean>
Check if a feature flag is enabled. Uses caching if configured.
Promise<FeatureFlagVariant>
Get the variant for a multi-variate flag
Promise<Map<string, boolean>>
Batch evaluate multiple flags at once
Promise<boolean>
Resolve a FeatureFlagRef (string or object) to a boolean

Best Practices

The static adapter requires no external dependencies and gives you instant, predictable flag values during development. Switch to a real adapter (Split.io, LaunchDarkly, Unleash) for staging and production.
When the adapter throws (network error, service down), defaultValue controls whether the flag is treated as enabled or disabled. Set it globally or per-flag:
External adapters involve network calls. Use cacheStrategy: 'session' to avoid repeated evaluations:
For features that should remain available even when the flag service is down, use the object form with defaultValue: true:
This ensures the tool stays available if the adapter throws.

Complete Example


Source Code

View the feature flags plugin source code

Plugin Guide

Learn more about FrontMCP plugins

Remember Plugin

For session memory storage

Approval Plugin

For tool authorization workflows