Guide · Feature Flags
MCP Tools for OpenFeature — Provider pattern, evaluation context, typed flag evaluation, hooks, and health monitoring
OpenFeature is a CNCF standard that decouples feature flag evaluation from the underlying flag management system (LaunchDarkly, Unleash, Flagsmith, or any other provider). MCP tools built against OpenFeature can switch flag backends without rewriting tool logic. The critical design concept is the provider lifecycle: a provider transitions through NOT_READY → READY → STALE → ERROR states, and evaluations called before the provider reaches READY silently return default values — not errors. MCP tools must check provider.status === ProviderStatus.READY before trusting evaluation results.
TL;DR
Install: npm install @openfeature/server-sdk. Register a provider: await OpenFeature.setProviderAndWait(provider). Get a client: const client = OpenFeature.getClient(). Evaluate: await client.getBooleanValue(flagKey, false, context). For evaluation reason: await client.getBooleanDetails(flagKey, false, context) → returns {value, reason, errorCode, flagMetadata}. Provider status: OpenFeature.getProviderStatus() → ProviderStatus.READY | STALE | ERROR | NOT_READY. Always call setProviderAndWait() (not setProvider()) to block until initialization completes.
OpenFeature architecture for MCP tool authors
OpenFeature has three layers: the API (global singleton, holds the registered provider), the Client (evaluation surface, scoped to a domain), and the Provider (the bridge to the actual flag management system). The API layer is vendor-neutral and stable. The Provider layer is vendor-specific — you register a LaunchDarkly provider, or an Unleash provider, and the Client calls evaluate against whichever provider is registered.
Domain-scoped clients allow different providers per domain — useful in multi-tenant MCP servers where different tenants use different flag systems:
import { OpenFeature, ProviderStatus } from '@openfeature/server-sdk';
// Provider packages are separate: @openfeature/launchdarkly-server-provider,
// @openfeature/unleash-server-provider, etc.
// Global default provider (for single flag system setups)
async function initOpenFeature(provider) {
// setProviderAndWait blocks until provider.initialize() resolves
// setProvider() is fire-and-forget — evaluations during init return defaults silently
await OpenFeature.setProviderAndWait(provider);
}
// Domain-scoped provider (for multi-tenant or multi-system setups)
async function initDomainProvider(domain, provider) {
await OpenFeature.setProviderAndWait(provider, domain);
}
// Get client scoped to a domain (or global if no domain specified)
function getClient(domain, targetingAttributes = {}) {
const client = domain
? OpenFeature.getClient(domain)
: OpenFeature.getClient();
// Evaluation context set at client level applies to all evaluations from this client
// Can be overridden per-evaluation with a local context
if (Object.keys(targetingAttributes).length > 0) {
client.setContext({
targetingKey: targetingAttributes.userId,
...targetingAttributes,
});
}
return client;
}
// Check provider readiness before any evaluation
function assertProviderReady(domain) {
const status = domain
? OpenFeature.getProviderStatus(domain)
: OpenFeature.getProviderStatus();
if (status !== ProviderStatus.READY) {
throw new Error(`OpenFeature provider status: ${status} — evaluations would return defaults`);
}
}
Typed flag evaluation and evaluation details
OpenFeature provides four typed evaluation methods: getBooleanValue, getStringValue, getNumberValue, and getObjectValue. Each has a corresponding *Details method that returns the full evaluation details including the reason the value was returned and any error that occurred. MCP tools should always use the *Details variants for logging and transparency — the plain *Value methods discard the reason and error, making it impossible to tell whether a default was returned because targeting rules selected it or because an error occurred.
// Typed evaluation with full details for transparency
async function evaluateFlag(client, flagKey, defaultValue, context = {}) {
// Merge local context with client-level context
const evalContext = Object.keys(context).length > 0
? { ...context }
: undefined;
// Choose the Details method based on the default value type
let details;
if (typeof defaultValue === 'boolean') {
details = await client.getBooleanDetails(flagKey, defaultValue, evalContext);
} else if (typeof defaultValue === 'string') {
details = await client.getStringDetails(flagKey, defaultValue, evalContext);
} else if (typeof defaultValue === 'number') {
details = await client.getNumberDetails(flagKey, defaultValue, evalContext);
} else {
details = await client.getObjectDetails(flagKey, defaultValue, evalContext);
}
return {
flagKey,
value: details.value,
reason: details.reason,
// reason ∈ STATIC | DEFAULT | TARGETING_MATCH | SPLIT | CACHED |
// DISABLED | UNKNOWN | ERROR
variant: details.variant, // variation name (if provider supports it)
errorCode: details.errorCode, // present when reason = ERROR
// errorCode ∈ PROVIDER_NOT_READY | FLAG_NOT_FOUND | PARSE_ERROR |
// TYPE_MISMATCH | TARGETING_KEY_MISSING | INVALID_CONTEXT |
// GENERAL
errorMessage: details.errorMessage, // human-readable error description
flagMetadata: details.flagMetadata, // provider-specific metadata
};
}
// Batch evaluation — evaluate multiple flags in one logical operation
async function evaluateMultiple(client, flags, context = {}) {
const results = await Promise.allSettled(
flags.map(({ key, defaultValue }) => evaluateFlag(client, key, defaultValue, context))
);
return flags.map((flag, i) => {
const result = results[i];
return result.status === 'fulfilled'
? result.value
: { flagKey: flag.key, value: flag.defaultValue, reason: 'ERROR',
errorMessage: result.reason?.message };
});
}
Evaluation context: the targeting key
The evaluation context is the set of attributes passed to the provider for flag targeting. The targetingKey is the mandatory field — most providers use it as the primary identifier for bucketing (percentage rollouts are deterministically computed from this key). Additional attributes are provider-specific: LaunchDarkly uses arbitrary context attributes, Unleash uses userId/sessionId/appName/remoteAddress, Flagsmith uses identity traits.
// Evaluation context structure
const context = {
targetingKey: 'user-123', // REQUIRED: stable user/entity identifier
// Standard fields (most providers recognize these)
email: 'alice@example.com',
country: 'US',
plan: 'enterprise',
// Provider-specific fields (passed through to provider's evaluation)
sessionId: 'sess-abc',
appName: 'mcp-server',
remoteAddress: '203.0.113.1',
// Multi-kind context (for LaunchDarkly multi-context)
// kind: 'user', // specify entity type when provider supports it
};
// Context hierarchy: transaction > client > global
// OpenFeature merges contexts in this order (transaction overrides client overrides global)
// Set global context (applies to all evaluations)
OpenFeature.setContext({ appVersion: '2.1.0', deploymentEnv: 'production' });
// Set per-request context (via hook or transaction context)
async function evaluateWithRequestContext(flagKey, defaultValue, requestUserId, requestPlan) {
const client = OpenFeature.getClient();
// Per-evaluation context (highest priority — overrides client and global)
return client.getBooleanDetails(flagKey, defaultValue, {
targetingKey: requestUserId,
plan: requestPlan,
});
}
Hooks: logging, auditing, and instrumentation
OpenFeature hooks allow cross-cutting concerns to be added without modifying flag evaluation logic. Hooks run at four lifecycle points: before (before evaluation, can enrich context), after (after successful evaluation, can log the result), error (on evaluation error), and finally (always, whether success or error). MCP tools can use hooks to log every evaluation for audit trails without adding logging code to each tool handler.
// Audit logging hook — logs every flag evaluation with its reason
const auditHook = {
name: 'mcp-audit-logger',
// Called after every successful evaluation
after(hookContext, details) {
if (details.reason === 'ERROR') return; // handled by error() hook
console.error(JSON.stringify({
event: 'flag_evaluated',
flagKey: hookContext.flagKey,
targetingKey: hookContext.context?.targetingKey,
value: details.value,
reason: details.reason,
variant: details.variant,
ts: new Date().toISOString(),
}));
},
// Called when evaluation throws or returns errorCode
error(hookContext, error) {
console.error(JSON.stringify({
event: 'flag_eval_error',
flagKey: hookContext.flagKey,
targetingKey: hookContext.context?.targetingKey,
errorCode: error?.code,
message: error?.message,
ts: new Date().toISOString(),
}));
},
};
// Latency tracking hook
const latencyHook = {
name: 'mcp-latency-tracker',
before(hookContext, hints) {
hints._startMs = Date.now();
},
finally(hookContext, hints) {
const latencyMs = Date.now() - (hints._startMs ?? Date.now());
if (latencyMs > 100) {
console.error(JSON.stringify({
event: 'flag_eval_slow',
flagKey: hookContext.flagKey,
latencyMs,
threshold: 100,
}));
}
},
};
// Register hooks at the global level (applies to all clients)
OpenFeature.addHooks(auditHook, latencyHook);
Health check: provider status vs flag evaluation health
OpenFeature has two distinct health dimensions. Provider status (READY/STALE/ERROR/NOT_READY) reflects whether the provider initialized and maintains its connection to the flag backend. Evaluation health is a separate concern: a provider can be in READY status but evaluate every flag to its default because the targeting key is missing from the context, or because every flag definition was deleted from the backend. A complete health probe checks both.
async function healthCheck(provider, domain) {
const results = await Promise.allSettled([
// 1. Provider status
(async () => {
const status = domain
? OpenFeature.getProviderStatus(domain)
: OpenFeature.getProviderStatus();
if (status === ProviderStatus.NOT_READY) {
throw new Error('Provider not initialized');
}
if (status === ProviderStatus.ERROR) {
throw new Error('Provider in ERROR state');
}
return {
kind: 'provider_status',
status,
providerName: provider.metadata?.name,
stale: status === ProviderStatus.STALE,
};
})(),
// 2. Synthetic flag evaluation — confirm evaluations actually work
(async () => {
const client = domain
? OpenFeature.getClient(domain)
: OpenFeature.getClient();
// Use a flag key that doesn't exist — expect FLAG_NOT_FOUND errorCode,
// not a system error. If provider is healthy, FLAG_NOT_FOUND is the
// correct response for an unknown flag.
const details = await client.getBooleanDetails(
'__alivemcp_probe_flag__',
false,
{ targetingKey: '__health_probe__' }
);
const providerResponded = details.errorCode === 'FLAG_NOT_FOUND'
|| details.reason !== 'ERROR';
if (!providerResponded) {
throw new Error(`Provider evaluation error: ${details.errorCode} — ${details.errorMessage}`);
}
return {
kind: 'evaluation',
probeResult: details.reason,
errorCode: details.errorCode,
providerResponded: true,
};
})(),
// 3. Provider-specific health (call the provider's native health method if available)
...(typeof provider.healthCheck === 'function' ? [(async () => {
const result = await provider.healthCheck();
return { kind: 'provider_native', ...result };
})()] : []),
]);
return {
healthy: results.every(r => r.status === 'fulfilled'),
components: results.map(r =>
r.status === 'fulfilled'
? { ...r.value, ok: true }
: { ok: false, error: r.reason?.message }
),
};
}
The synthetic evaluation with an unknown flag key is the most reliable health probe: if the provider responds with FLAG_NOT_FOUND, the evaluation path is fully operational. If it returns a different error code (e.g., PROVIDER_NOT_READY or GENERAL), the provider has a connectivity problem to the flag backend. This technique works regardless of which flag management system the provider connects to.
Set AliveMCP alert thresholds: critical when provider status is ERROR or synthetic evaluation returns GENERAL error; warning when provider status is STALE for more than 2 minutes (flag updates from the backend are not propagating) or evaluation latency exceeds 100ms consistently.
Common integration pitfalls
- setProvider() vs setProviderAndWait()
setProvider()is asynchronous and fire-and-forget — the provider initializes in the background. Evaluations during initialization return the default value with reasonPROVIDER_NOT_READY, not an error. In an MCP server that starts handling requests immediately, this creates a window where all evaluations silently return defaults. Always usesetProviderAndWait()in your MCP server initialization, before registering tool handlers.- Evaluation reason DISABLED vs ERROR vs DEFAULT
- A flag that the backend has disabled (targeting off, kill-switch) returns reason
DISABLED. A flag where evaluation failed returns reasonERRORwitherrorCodeset. A flag where no targeting rule matched returns reasonDEFAULT. These are distinct states — a flag returningDEFAULTis not an error (it means the fallthrough variation applied). LogDISABLEDandERRORbut notDEFAULTorSTATIC. - Missing targetingKey causes silent defaults
- If evaluation context is missing the
targetingKey, providers that require it (for percentage rollouts, user-targeting rules) return the default value with reasonERRORanderrorCode: TARGETING_KEY_MISSING. Without using*Detailsmethods, this looks identical to a correctly evaluated default. Always validate thattargetingKeyis present in the context before evaluation in code paths where user identity is required. - Domain-scoped clients don't share providers
- Providers registered with a domain string are independent from the global provider. Calling
OpenFeature.getClient('payments')uses the provider registered for domain'payments', not the global default. Forgetting to register a domain-specific provider means the domain client falls back to the global provider — which may use a different flag system than intended.
Related guides
- MCP tools for LaunchDarkly — flag evaluation, targeting rules, and SDK vs REST API
- MCP tools for Unleash — open-source feature toggles, activation strategies, and Admin vs Client API
- MCP tools for Flagsmith — feature states, remote config, and identity-based evaluation
- MCP tools for Split.io — treatments, traffic allocation, dynamic config, and experiment tracking
- MCP server health check patterns — composite endpoints and failure classification
- MCP server uptime monitoring — probes, intervals, and alert routing