Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.agentfront.dev/llms.txt

Use this file to discover all available pages before exploring further.

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 tool/call invocations that bypass the list filter — no sneaking past disabled flags

Installation

npm install @frontmcp/plugin-feature-flags

How It Works

1

Hook Registration

The plugin registers hooks on list flows for tools, resources, prompts, and skills
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. An execution gate also blocks direct tool/call for disabled flags.

Quick Start

Basic Setup

import { FrontMcp, App } from '@frontmcp/sdk';
import { FeatureFlagPlugin } from '@frontmcp/plugin-feature-flags';

@App({
  id: 'my-app',
  name: 'My App',
  plugins: [
    FeatureFlagPlugin.init({
      adapter: 'static',
      flags: { 'beta-search': true, 'experimental-agent': false },
    }),
  ],
  tools: [/* your tools */],
})
class MyApp {}

@FrontMcp({
  info: { name: 'My Server', version: '1.0.0' },
  apps: [MyApp],
})
export default class Server {}

Annotating a Tool

import { Tool, ToolContext } from '@frontmcp/sdk';
import { z } from '@frontmcp/sdk';

@Tool({
  name: 'beta-search',
  description: 'Search with beta features enabled',
  inputSchema: { query: z.string() },
  featureFlag: 'beta-search', // Hidden when flag is off
})
export default class BetaSearchTool extends ToolContext {
  async execute(input: { query: string }) {
    return `Beta results for: ${input.query}`;
  }
}

Programmatic Flag Checks

The plugin extends all execution contexts with this.featureFlags:
@Tool({
  name: 'smart-tool',
  description: 'A tool that adapts based on flags',
  inputSchema: { query: z.string() },
})
export default class SmartTool extends ToolContext {
  async execute(input: { query: string }) {
    const useBeta = await this.featureFlags.isEnabled('beta-algorithm');

    if (useBeta) {
      return this.betaSearch(input.query);
    }
    return this.standardSearch(input.query);
  }
}

Adapters

Static

Best for: Development, testing, and demos with fixed flag values.
FeatureFlagPlugin.init({
  adapter: 'static',
  flags: {
    'beta-search': true,
    'experimental-agent': false,
    'premium-feature': true,
  },
})

Split.io

FeatureFlagPlugin.init({
  adapter: 'splitio',
  config: {
    apiKey: process.env.SPLITIO_API_KEY!,
  },
})
Requires the @splitsoftware/splitio peer dependency:
npm install @splitsoftware/splitio

LaunchDarkly

FeatureFlagPlugin.init({
  adapter: 'launchdarkly',
  config: {
    sdkKey: process.env.LAUNCHDARKLY_SDK_KEY!,
  },
})
Requires the @launchdarkly/node-server-sdk peer dependency:
npm install @launchdarkly/node-server-sdk

Unleash

FeatureFlagPlugin.init({
  adapter: 'unleash',
  config: {
    url: process.env.UNLEASH_URL!,
    appName: 'my-mcp-server',
    apiKey: process.env.UNLEASH_API_KEY,
  },
})
Requires the unleash-client peer dependency:
npm install unleash-client

Custom

Provide your own adapter implementing the FeatureFlagAdapter interface:
import type { FeatureFlagAdapter } from '@frontmcp/plugin-feature-flags';

const myAdapter: FeatureFlagAdapter = {
  async initialize() { /* connect to your service */ },
  async isEnabled(flagKey, context) {
    return await myService.checkFlag(flagKey, context.userId);
  },
  async getVariant(flagKey, context) {
    const variant = await myService.getVariant(flagKey, context.userId);
    return { name: variant.name, value: variant.value, enabled: variant.enabled };
  },
  async evaluateFlags(flagKeys, context) {
    const results = new Map<string, boolean>();
    for (const key of flagKeys) {
      results.set(key, await myService.checkFlag(key, context.userId));
    }
    return results;
  },
  async destroy() { /* cleanup */ },
};

FeatureFlagPlugin.init({
  adapter: 'custom',
  adapterInstance: myAdapter,
})
The FeatureFlagAdapter interface:
interface FeatureFlagAdapter {
  initialize(): Promise<void>;
  isEnabled(flagKey: string, context: FeatureFlagContext): Promise<boolean>;
  getVariant(flagKey: string, context: FeatureFlagContext): Promise<FeatureFlagVariant>;
  evaluateFlags(flagKeys: string[], context: FeatureFlagContext): Promise<Map<string, boolean>>;
  destroy(): Promise<void>;
}

Annotating Capabilities

Add featureFlag to any tool, resource, prompt, or skill metadata. Capabilities with disabled flags are hidden from list responses and blocked from direct invocation.

String Shorthand

@Tool({ name: 'beta-search', featureFlag: 'beta-search' })
@Resource({ uri: 'data://beta', featureFlag: 'beta-data' })
@Prompt({ name: 'beta-prompt', featureFlag: 'beta-prompt' })
@Skill({ name: 'beta-skill', featureFlag: 'beta-skill' })

Object Form

Use the object form to specify a defaultValue — the fallback when the adapter throws or the flag is unknown:
@Tool({
  name: 'critical-tool',
  featureFlag: { key: 'critical-tool', defaultValue: true }, // fail-open
})

FeatureFlagRef Fields

FieldTypeDescription
keystringThe flag key to evaluate
defaultValuebooleanFallback value when evaluation fails (default: false)
When using the string shorthand (featureFlag: 'key'), defaultValue is false.

Plugin Options

adapter
'static' | 'splitio' | 'launchdarkly' | 'unleash' | 'custom'
required
The feature flag provider to use
flags
Record<string, boolean | FeatureFlagVariant>
Static flag values (only used with adapter: 'static')
config
object
Provider-specific configuration (varies by adapter):
  • Split.io: { apiKey: string }
  • LaunchDarkly: { sdkKey: string }
  • Unleash: { url: string, appName: string, apiKey?: string }
adapterInstance
FeatureFlagAdapter
Custom adapter instance (only used with adapter: 'custom')
defaultValue
boolean
default:"false"
Global fallback when the adapter throws an error during evaluation
cacheStrategy
'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
cacheTtlMs
number
default:"30000"
Cache TTL in milliseconds (used with cacheStrategy: 'session')
userIdResolver
(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.
attributesResolver
(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.

API Reference

FeatureFlagAccessor Methods

Access via this.featureFlags in any execution context (ToolContext, AgentContext, etc.).
isEnabled(flagKey, defaultValue?)
Promise<boolean>
Check if a feature flag is enabled. Uses caching if configured.
const enabled = await this.featureFlags.isEnabled('beta-feature');
const safe = await this.featureFlags.isEnabled('critical-feature', true); // fail-open
getVariant(flagKey)
Promise<FeatureFlagVariant>
Get the variant for a multi-variate flag
const variant = await this.featureFlags.getVariant('search-algorithm');
console.log(variant.name);    // e.g., 'v2'
console.log(variant.value);   // e.g., { boost: 1.5 }
console.log(variant.enabled); // true
evaluateFlags(flagKeys)
Promise<Map<string, boolean>>
Batch evaluate multiple flags at once
const results = await this.featureFlags.evaluateFlags(['flag-a', 'flag-b', 'flag-c']);
if (results.get('flag-a')) {
  // flag-a is enabled
}
resolveRef(ref)
Promise<boolean>
Resolve a FeatureFlagRef (string or object) to a boolean
const ref = { key: 'my-flag', defaultValue: true };
const enabled = await this.featureFlags.resolveRef(ref);

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.
FeatureFlagPlugin.init({
  adapter: 'static',
  flags: { 'beta-tools': true, 'experimental-agent': true },
})
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:
// Global default: treat unknown flags as disabled
FeatureFlagPlugin.init({
  adapter: 'launchdarkly',
  config: { sdkKey: '...' },
  defaultValue: false,
})

// Per-flag: critical features should fail-open
@Tool({
  name: 'core-tool',
  featureFlag: { key: 'core-tool', defaultValue: true },
})
External adapters involve network calls. Use cacheStrategy: 'session' to avoid repeated evaluations:
FeatureFlagPlugin.init({
  adapter: 'splitio',
  config: { apiKey: '...' },
  cacheStrategy: 'session',
  cacheTtlMs: 30000, // 30 seconds
})
For features that should remain available even when the flag service is down, use the object form with defaultValue: true:
@Tool({
  name: 'payment-processor',
  featureFlag: { key: 'payment-processor', defaultValue: true },
})
This ensures the tool stays available if the adapter throws.

Complete Example

import { FrontMcp, App, Tool, ToolContext, Resource, ResourceContext, Prompt, PromptContext } from '@frontmcp/sdk';
import { FeatureFlagPlugin } from '@frontmcp/plugin-feature-flags';
import { z } from '@frontmcp/sdk';

// Configure with static adapter
const featureFlagPlugin = FeatureFlagPlugin.init({
  adapter: 'static',
  flags: {
    'beta-search': true,
    'experimental-agent': false,
    'premium-reports': true,
  },
  defaultValue: false,
});

// Tool gated by a feature flag
@Tool({
  name: 'beta-search',
  description: 'Search with beta algorithm',
  inputSchema: { query: z.string() },
  featureFlag: 'beta-search',
})
class BetaSearchTool extends ToolContext {
  async execute(input: { query: string }) {
    return `Beta results for: ${input.query}`;
  }
}

// Tool with fail-open behavior
@Tool({
  name: 'always-on-tool',
  description: 'A tool that defaults to enabled',
  inputSchema: { input: z.string() },
  featureFlag: { key: 'always-on', defaultValue: true },
})
class AlwaysOnTool extends ToolContext {
  async execute(input: { input: string }) {
    return `Processed: ${input.input}`;
  }
}

// Tool with programmatic flag check
@Tool({
  name: 'adaptive-tool',
  description: 'Adapts behavior based on flags',
  inputSchema: { data: z.string() },
})
class AdaptiveTool extends ToolContext {
  async execute(input: { data: string }) {
    const usePremium = await this.featureFlags.isEnabled('premium-reports');

    if (usePremium) {
      return `Premium report for: ${input.data}`;
    }
    return `Standard report for: ${input.data}`;
  }
}

// Resource gated by feature flag
@Resource({
  uri: 'report://premium-status',
  name: 'Premium Status Report',
  featureFlag: 'premium-reports',
})
class PremiumStatusResource extends ResourceContext {
  async read() {
    return { contents: [{ uri: 'report://premium-status', text: 'Premium data...' }] };
  }
}

// Prompt gated by feature flag
@Prompt({
  name: 'experimental-analysis',
  description: 'Experimental analysis prompt',
  featureFlag: 'experimental-agent',
})
class ExperimentalPrompt extends PromptContext {
  async execute() {
    return { messages: [{ role: 'user' as const, content: { type: 'text' as const, text: 'Analyze...' } }] };
  }
}

@App({
  id: 'flagged-app',
  name: 'Flagged App',
  plugins: [featureFlagPlugin],
  tools: [BetaSearchTool, AlwaysOnTool, AdaptiveTool],
  resources: [PremiumStatusResource],
  prompts: [ExperimentalPrompt],
})
class FlaggedApp {}

@FrontMcp({
  info: { name: 'Feature Flag Server', version: '1.0.0' },
  apps: [FlaggedApp],
  http: { port: 3000 },
})
export default class Server {}

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