Guide · Feature Flags

MCP Tools for Flagsmith — Feature states, remote config, identity-based evaluation, segments, and health monitoring

Flagsmith combines two distinct capabilities in one platform: feature flags (boolean on/off per environment) and remote configuration (arbitrary string values stored alongside each flag). MCP tools that treat Flagsmith as a pure on/off system miss the remote config dimension entirely — a flag can be "enabled" with an empty value, returning the feature toggle's boolean state but discarding the configuration payload that controls how that feature behaves. The key field is feature_state_value, not just enabled, and they change independently.

TL;DR

API base: https://api.flagsmith.com/api/v1/ (or self-hosted URL). Auth: X-Environment-Key: <env-key> for environment reads (SDK operations); Authorization: Api-Key <api-key> for management reads. Get all flags: GET /flags/. Get identity flags: POST /identities/ with {"identifier": "user-123", "traits": [...]}. Health: GET /health/{"status": "ok"}. Each flag response has two fields that matter: enabled (boolean) and feature_state_value (string/null — the remote config value). Always read both.

Flagsmith architecture for MCP tool authors

Flagsmith organises state into: Organisation → Project → Environment → Feature → Feature State. A feature is defined at the project level (it exists once), but its state — whether it's enabled and what value it carries — is defined per environment. You never query features directly; you query the feature state in a specific environment.

Two credential types control different access levels:

// Environment-scoped reads (SDK-level, most MCP tools use this)
async function flagsmithEnv(envKey, baseUrl, path, method = 'GET', body = null) {
  const base = (baseUrl || 'https://api.flagsmith.com').replace(/\/$/, '');
  const res = await fetch(`${base}/api/v1${path}`, {
    method,
    headers: {
      'X-Environment-Key': envKey,
      'Content-Type': 'application/json',
    },
    body: body ? JSON.stringify(body) : null,
    signal: AbortSignal.timeout(10_000),
  });
  if (!res.ok) {
    const text = await res.text().catch(() => '');
    throw new Error(`Flagsmith ENV API ${method} ${path} → ${res.status}: ${text.slice(0, 200)}`);
  }
  return res.json();
}

// Management-level reads (project/org scope)
async function flagsmithMgmt(apiKey, baseUrl, path) {
  const base = (baseUrl || 'https://api.flagsmith.com').replace(/\/$/, '');
  const res = await fetch(`${base}/api/v1${path}`, {
    headers: {
      'Authorization': `Api-Key ${apiKey}`,
      'Content-Type': 'application/json',
    },
    signal: AbortSignal.timeout(10_000),
  });
  if (!res.ok) {
    throw new Error(`Flagsmith MGMT API GET ${path} → ${res.status} ${res.statusText}`);
  }
  return res.json();
}

Flags vs remote config: reading both dimensions

Every Flagsmith feature state has two independent dimensions: the enabled boolean and the feature_state_value string. A feature named max_batch_size might be enabled with a value of "500" in production and enabled with "50" in staging — the enabled state is the same, but the behavior differs by value. MCP tools must surface both fields explicitly.

// Get all flags for the current environment
async function getFlags(envKey, baseUrl) {
  const flags = await flagsmithEnv(envKey, baseUrl, '/flags/');

  return flags.map(flag => ({
    featureName: flag.feature.name,
    featureId: flag.feature.id,
    featureType: flag.feature.type,  // STANDARD (boolean) | MULTIVARIATE
    enabled: flag.enabled,
    value: flag.feature_state_value,  // string | number | null — the remote config value
    // Critical: value can be non-null even when enabled=false
    // (the value is stored regardless of the enabled state)
  }));
}

// Get a specific flag by name
async function getFlag(envKey, baseUrl, featureName) {
  const flags = await getFlags(envKey, baseUrl);
  const flag = flags.find(f => f.featureName === featureName);
  if (!flag) {
    return { found: false, enabled: false, value: null };
  }
  return { found: true, ...flag };
}

// Parse the value with type awareness
function parseValue(rawValue, expectedType = 'string') {
  if (rawValue === null || rawValue === undefined) return null;
  switch (expectedType) {
    case 'number': return Number(rawValue);
    case 'boolean': return rawValue === 'true' || rawValue === true;
    case 'json': {
      try { return JSON.parse(rawValue); }
      catch { return null; }
    }
    default: return String(rawValue);
  }
}

Identity-based evaluation with traits

The most powerful Flagsmith capability is identity-based evaluation: instead of environment-level flags (same for all users), you POST an identity with traits to get per-user flag states. Traits are key-value pairs associated with an identity that targeting rules (segments) evaluate against. An identity that matches a segment gets the segment's overridden flag state, which can differ from the environment default.

// Get flags for a specific user identity with their traits
async function getIdentityFlags(envKey, baseUrl, identifier, traits = []) {
  // POST to /identities/ creates or updates the identity and returns flags
  const result = await flagsmithEnv(envKey, baseUrl, '/identities/', 'POST', {
    identifier,
    traits: traits.map(t => ({
      trait_key: t.key,
      trait_value: t.value,  // string | number | boolean
    })),
  });

  return {
    identity: result.identifier,
    flags: (result.flags ?? []).map(flag => ({
      featureName: flag.feature.name,
      enabled: flag.enabled,
      value: flag.feature_state_value,
      // Identity flags that differ from env defaults got here via segment override
    })),
    traits: result.traits ?? [],
  };
}

// Update traits for an existing identity (without re-fetching flags)
async function setTrait(envKey, baseUrl, identifier, traitKey, traitValue) {
  return flagsmithEnv(envKey, baseUrl, '/traits/', 'POST', {
    identity: { identifier },
    trait_key: traitKey,
    trait_value: traitValue,
  });
}

// Example: get a specific flag for a user with context
async function evaluateForUser(envKey, baseUrl, identifier, featureName, userTraits = []) {
  const result = await getIdentityFlags(envKey, baseUrl, identifier, userTraits);
  const flag = result.flags.find(f => f.featureName === featureName);
  return flag ?? { featureName, enabled: false, value: null };
}

A critical behavior: calling POST /identities/ creates the identity if it doesn't exist and persists the traits you provide. This is not a stateless read — every call can mutate server-side state. For read-only evaluation probes, use GET /identities/?identifier=user-123 to read without creating or updating traits.

Segments and multivariate flags

Segments are named groups of users defined by trait-based rules. A segment rule example: "plan equals enterprise AND country in [US, CA, GB]". When an identity's traits match a segment, any flag that has a segment override for that segment returns the overridden state for that identity. MCP tools can list segments to show which user groups have distinct flag behavior.

// List segments defined in a project (requires management API key)
async function listSegments(apiKey, baseUrl, projectId) {
  const data = await flagsmithMgmt(apiKey, baseUrl, `/projects/${projectId}/segments/`);
  return (data.results ?? []).map(s => ({
    id: s.id,
    name: s.name,
    description: s.description,
    rules: s.rules,  // nested rule tree: {type: 'ALL'|'ANY'|'NONE', rules: [{property, operator, value}]}
  }));
}

// Multivariate flags: instead of a single enabled+value, the flag has
// multiple variations each with a percentage weight
async function getMultivariateFlag(envKey, baseUrl, featureName) {
  // For multivariate flags, the /flags/ response has a different shape
  const flags = await flagsmithEnv(envKey, baseUrl, '/flags/');
  const flag = flags.find(f => f.feature.name === featureName);
  if (!flag) return null;

  if (flag.feature.type !== 'MULTIVARIATE') {
    return { type: 'standard', enabled: flag.enabled, value: flag.feature_state_value };
  }

  // For multivariate, get the variation options separately
  const mvOptions = await flagsmithEnv(
    envKey, baseUrl,
    `/features/featurestates/?feature=${flag.feature.id}`
  );

  return {
    type: 'multivariate',
    enabled: flag.enabled,
    controlValue: flag.feature_state_value,  // the control variation value
    variations: (mvOptions.results ?? []).filter(o => o.multivariate_feature_option !== null),
  };
}

Health check: what GET /health/ misses

Flagsmith's GET /health/ returns {"status": "ok"} when the process is running. It does not verify: whether the database is responding, whether identity evaluation (the segment matching engine) is functioning, or whether flag state changes are being persisted. Self-hosted Flagsmith deployments using PostgreSQL are particularly vulnerable to the "process up, DB read replicas stale" failure mode that affects many stateful services.

async function healthCheck(envKey, apiKey, baseUrl, projectId) {
  const results = await Promise.allSettled([
    // 1. Process liveness
    (async () => {
      const res = await fetch(`${(baseUrl || 'https://api.flagsmith.com').replace(/\/$/, '')}/health/`);
      const data = await res.json();
      if (data.status !== 'ok') throw new Error(`Health status: ${data.status}`);
      return { kind: 'process', status: data.status };
    })(),

    // 2. Flag store readable (exercises DB read path)
    (async () => {
      const flags = await flagsmithEnv(envKey, baseUrl, '/flags/');
      return {
        kind: 'flag_store',
        flagCount: flags.length,
      };
    })(),

    // 3. Identity evaluation (exercises segment matching engine)
    (async () => {
      const result = await getIdentityFlags(
        envKey, baseUrl,
        '__alivemcp_health_probe__',
        [{ key: 'probe', value: 'true' }]
      );
      return {
        kind: 'identity_eval',
        flagCount: result.flags.length,
        traitsAccepted: true,
      };
    })(),

    // 4. Management API (exercises org/project read path — requires apiKey)
    ...(apiKey ? [(async () => {
      const projects = await flagsmithMgmt(apiKey, baseUrl, '/projects/');
      return {
        kind: 'mgmt_api',
        projectCount: (projects.results ?? projects).length,
      };
    })()] : []),
  ]);

  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 identity evaluation probe is the most comprehensive check: it creates a synthetic identity with a probe trait, then retrieves flags for it. If segment matching is broken (e.g., a bad migration corrupted segment rules), identity evaluation returns incorrect results while the flags endpoint returns correctly. The probe identity __alivemcp_health_probe__ should have no segment overrides — if it returns flags that differ from environment defaults, that's a signal of unexpected targeting state.

Set AliveMCP alert thresholds: critical when the flags endpoint returns an error or the process health endpoint returns anything other than ok; warning when flag count drops more than 10% from baseline (possible bulk deletion) or identity evaluation takes more than 2 seconds (segment matching engine degraded).

Common integration pitfalls

Environment key vs API key scope confusion
The Environment key (ser.xxx) is scoped to one environment. Using it to list flags gives only that environment's flags — not all environments. If a flag was added to the project after the Environment key was created, it appears in GET /flags/ with the environment's default state (disabled, null value), not an error. There's no way to know a flag exists in other environments without the management API.
POST /identities/ creates and persists identities
Unlike LaunchDarkly's context evaluation (stateless), POST /identities/ in Flagsmith persists the identity and its traits in the database. Every call is a write. Use GET /identities/?identifier=xxx for read-only access to existing identities. In high-traffic scenarios, repeatedly posting the same identity with the same traits adds unnecessary write load to Flagsmith's database.
feature_state_value is always a string
Even when you configured a flag value as a number in the Flagsmith UI, feature_state_value returns a string ("42", not 42). MCP tools must parse to the expected type explicitly. Passing a string "false" to a boolean check fails silently — Boolean("false") === true in JavaScript.
Segment changes are not retroactive on cached SDKs
Server-side Flagsmith SDKs cache flag states on initialization and refresh on an interval. If a user's traits don't match any segment at cache time but later would match (because a new segment was added), the SDK won't reflect this until the next refresh cycle. Use POST /identities/ directly (server-to-server, not cached) for real-time segment evaluation.

Related guides