Guide · Feature Flags

MCP Tools for Unleash — Feature toggle evaluation, activation strategies, Admin vs Client API, and health monitoring

Unleash exposes two separate API surfaces that require different credentials and serve different purposes: the Admin API (/api/admin/*) for flag management operations, and the Client API (/api/client/*) for SDK synchronization. MCP tools almost always use the Admin API, but the most important health signal — whether Unleash is actually evaluating toggles and recording impressions — is only visible through the Client API polling cycle. Unleash's GET /health endpoint returns {"health": "GOOD"} even when the database connection has dropped for features that aren't cached in memory, making a deeper composite health probe essential.

TL;DR

Auth: Authorization: Bearer <admin-token> for Admin API; Authorization: <client-token> for Client API (no Bearer prefix). Admin API base: https://your-unleash-instance/api/admin/. List toggles: GET /api/admin/features. Get one toggle: GET /api/admin/features/{toggleName}. Enable in environment: POST /api/admin/features/{toggleName}/environments/{environmentName}/on. Health: GET /health{"health":"GOOD"}. Metrics: GET /api/admin/metrics/feature-toggles/raw for impression counts per toggle per environment.

Unleash architecture for MCP tool authors

Unleash's toggle model is: project → feature toggle → strategy → activation rule. Every toggle lives in a project, has one or more strategies per environment, and each strategy has an activation rule (the condition that must be true for the toggle to evaluate to enabled). Environments are cross-cutting: the same toggle can be enabled in production and disabled in staging, with different strategies per environment.

The two APIs serve different audiences. The Admin API requires an admin token and is for human-facing operations: CRUD on toggles, enabling/disabling in environments, reading audit events. The Client API requires a client token and is for SDK polling: it returns a snapshot of all toggle configurations in a specific environment, which SDKs cache in memory and evaluate locally.

// Admin API client — for flag management
async function unleashAdmin(adminToken, instanceUrl, path, method = 'GET', body = null) {
  const base = instanceUrl.replace(/\/$/, '');
  const res = await fetch(`${base}${path}`, {
    method,
    headers: {
      'Authorization': `Bearer ${adminToken}`,  // Bearer prefix required for Admin API
      '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(`Unleash Admin API ${method} ${path} → ${res.status}: ${text.slice(0, 200)}`);
  }
  if (method === 'DELETE' || res.status === 204) return null;
  return res.json();
}

// Client API — for SDK-compatible snapshot retrieval
async function unleashClient(clientToken, instanceUrl, environmentName) {
  const base = instanceUrl.replace(/\/$/, '');
  // Client API uses no Bearer prefix — just the raw token
  const res = await fetch(`${base}/api/client/features`, {
    headers: {
      'Authorization': clientToken,
      'Unleash-Client-Spec': '4.3.0',  // declare SDK version
      'Unleash-AppName': 'mcp-health-probe',
      'Unleash-InstanceId': 'probe-1',
    },
    signal: AbortSignal.timeout(10_000),
  });
  if (!res.ok) {
    throw new Error(`Unleash Client API → ${res.status} ${res.statusText}`);
  }
  return res.json();  // {version, features: [...], segments: [...]}
}

Toggle types and activation strategies

Unleash has five toggle types that communicate intent to developers: release (temporary, for feature rollouts), experiment (temporary, for A/B tests with impression data), operational (permanent, for circuit-breaker / kill-switch patterns), kill-switch (permanent, on/off with no gradual rollout), and permission (permanent, for access control based on user attributes). The type is metadata only — it doesn't change evaluation behavior, but it signals when a temporary toggle should be cleaned up.

Activation strategies define the rule: a toggle is enabled if any strategy evaluates to true. Available strategies:

// List all toggles in a project
async function listToggles(adminToken, instanceUrl, projectId = 'default') {
  const data = await unleashAdmin(adminToken, instanceUrl, `/api/admin/projects/${projectId}/features`);
  return (data.features ?? []).map(f => ({
    name: f.name,
    type: f.type,
    description: f.description,
    archived: f.archived,
    stale: f.stale,  // manually marked as past its expected removal date
    createdAt: f.createdAt,
    lastSeenAt: f.lastSeenAt,  // when the toggle was last evaluated by an SDK
  }));
}

// Get detailed toggle configuration including strategies per environment
async function getToggle(adminToken, instanceUrl, toggleName) {
  const toggle = await unleashAdmin(adminToken, instanceUrl, `/api/admin/features/${toggleName}`);

  return {
    name: toggle.name,
    type: toggle.type,
    description: toggle.description,
    stale: toggle.stale,
    archived: toggle.archived,
    environments: (toggle.environments ?? []).map(env => ({
      name: env.name,
      enabled: env.enabled,
      strategies: (env.strategies ?? []).map(s => ({
        name: s.name,                     // strategy type (see list above)
        parameters: s.parameters,         // strategy-specific config
        segments: s.segments ?? [],       // segment IDs applied to this strategy
        constraints: s.constraints ?? [], // attribute constraints (age > 18, plan = pro)
      })),
      variants: env.variants ?? [],
    })),
  };
}

// Enable or disable a toggle in a specific environment
async function setToggleState(adminToken, instanceUrl, toggleName, environmentName, enable) {
  const action = enable ? 'on' : 'off';
  return unleashAdmin(
    adminToken, instanceUrl,
    `/api/admin/features/${toggleName}/environments/${environmentName}/${action}`,
    'POST'
  );
}

Variants for multivariate flags

Unleash supports variants — named values returned alongside the enabled/disabled evaluation result. Variants allow A/B/n testing where users get different experiences within the same toggle. Variants have weights (summing to 1000, not 100 in Unleash's model), optional overrides for specific users, and a payload (string, JSON, CSV, or number).

// Get variants for a toggle in a specific environment
async function getVariants(adminToken, instanceUrl, toggleName, environmentName) {
  const toggle = await getToggle(adminToken, instanceUrl, toggleName);
  const env = toggle.environments.find(e => e.name === environmentName);
  if (!env) throw new Error(`Environment ${environmentName} not found for toggle ${toggleName}`);

  return env.variants.map(v => ({
    name: v.name,
    weight: v.weight,          // 0–1000 (total across all variants = 1000)
    weightType: v.weightType,  // variable (adjusts to maintain sum) | fix
    payload: v.payload,        // {type: 'string'|'json'|'csv'|'number', value: '...'}
    overrides: v.overrides ?? [],  // user/group IDs that always get this variant
  }));
}

// SDK-compatible evaluation via Client API
// (for tools that need to simulate what an SDK would evaluate)
async function evaluateToggle(clientToken, instanceUrl, toggleName, context) {
  const snapshot = await unleashClient(clientToken, instanceUrl);
  const feature = (snapshot.features ?? []).find(f => f.name === toggleName);

  if (!feature) {
    return { enabled: false, variant: { name: 'disabled', enabled: false }, reason: 'toggle_not_found' };
  }

  // The Client API snapshot doesn't evaluate — it returns raw configuration.
  // For server-side evaluation, use the Unleash Node.js SDK with a client token.
  // The snapshot is useful for: checking if a toggle exists in the Client API
  // (which is what SDKs see), and verifying variant configuration.
  return {
    found: true,
    enabledInEnvironment: feature.enabled,
    strategies: feature.strategies?.length ?? 0,
    variants: feature.variants?.length ?? 0,
  };
}

Impression data and metrics

Unleash tracks impression data — counts of how many times each toggle was evaluated to enabled vs disabled — per toggle per environment per application. This data powers the toggle's "last seen" timestamp and the stale toggle detector. The Admin API exposes raw metrics that MCP tools can surface to help teams identify dead toggles (never evaluated in 30 days = safe to archive).

// Get raw impression metrics for all toggles
async function getToggleMetrics(adminToken, instanceUrl) {
  // Returns toggle evaluation counts in the last period
  const metrics = await unleashAdmin(adminToken, instanceUrl, '/api/admin/metrics/feature-toggles/raw');
  return (metrics.data ?? []).map(m => ({
    toggleName: m.featureName,
    environment: m.environment,
    appName: m.appName,
    timestamp: m.timestamp,
    yes: m.yes,   // times evaluated as enabled
    no: m.no,     // times evaluated as disabled
  }));
}

// Find toggles that haven't been evaluated in the last N days (stale candidates)
async function findStaleToggles(adminToken, instanceUrl, projectId = 'default', dayThreshold = 30) {
  const toggles = await listToggles(adminToken, instanceUrl, projectId);
  const cutoff = new Date(Date.now() - dayThreshold * 86_400_000);

  return toggles.filter(t => {
    if (t.archived) return false;
    if (!t.lastSeenAt) return true;  // never evaluated
    return new Date(t.lastSeenAt) < cutoff;
  });
}

// Get audit events for a specific toggle (who changed what and when)
async function getToggleEvents(adminToken, instanceUrl, toggleName) {
  const events = await unleashAdmin(
    adminToken, instanceUrl,
    `/api/admin/events/${toggleName}`
  );
  return (events.events ?? []).map(e => ({
    id: e.id,
    type: e.type,     // feature-created, feature-updated, feature-environment-enabled, etc.
    createdAt: e.createdAt,
    createdBy: e.createdBy,
    data: e.data,
    preData: e.preData,  // state before the change (for diffs)
  }));
}

Health check: beyond GET /health

Unleash's GET /health endpoint returns {"health": "GOOD"} when the Unleash process is running. It does not verify: database connectivity, whether toggles are being evaluated (Client API polling active), or whether impression events are being recorded. A comprehensive health probe checks all three layers.

async function healthCheck(adminToken, clientToken, instanceUrl) {
  const results = await Promise.allSettled([
    // 1. Admin API — token validity and process liveness
    (async () => {
      const data = await unleashAdmin(adminToken, instanceUrl, '/api/admin/ui-config');
      // ui-config requires a valid admin token and a responsive DB (reads config from DB)
      return {
        kind: 'admin_api',
        unleashVersion: data.version,
        authType: data.authType,
      };
    })(),

    // 2. Client API — the surface SDKs use; this exercises DB reads for toggle configs
    (async () => {
      const snapshot = await unleashClient(clientToken, instanceUrl);
      return {
        kind: 'client_api',
        version: snapshot.version,
        toggleCount: (snapshot.features ?? []).length,
        segmentCount: (snapshot.segments ?? []).length,
      };
    })(),

    // 3. Database health via admin stats (fails if DB connection is lost)
    (async () => {
      const stats = await unleashAdmin(adminToken, instanceUrl, '/api/admin/instance-admin/statistics');
      return {
        kind: 'database',
        userCount: stats.users,
        toggleCount: stats.featureToggles,
        projectCount: stats.projects,
      };
    })(),
  ]);

  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 Client API check is the most operationally important: if SDKs can't poll toggle configurations, they'll continue serving stale cached state. An Unleash instance that fails the Client API check while passing the health endpoint is the most common production failure mode for self-hosted deployments — usually caused by a database read replica failure while the primary write replica remains healthy (health endpoint reads from the primary, toggle configs are served from the replica).

Set AliveMCP alert thresholds: critical when Admin API returns 401 (token revoked) or Client API returns non-200; warning when toggle count in the Client API snapshot drops more than 20% compared to the Admin API toggle count (may indicate a bulk archive or data loss event).

Common integration pitfalls

Bearer prefix on Client API calls
The Admin API requires Authorization: Bearer <token>. The Client API uses Authorization: <token> with no prefix. Passing a Bearer prefix to the Client API returns 401 silently on some Unleash versions, making it appear the client token is invalid when the issue is the header format.
Weight sum must equal 1000
When updating variants via the API, all variant weights must sum to exactly 1000 (not 100). Unleash uses per-mille weighting. A PATCH request where weights sum to anything other than 1000 returns a 400 error, but the error message sometimes says "Invalid strategy" rather than "Weight sum incorrect", which is misleading.
Stale toggle confusion
A toggle marked stale: true is still evaluated normally — "stale" is a workflow marker signaling a developer reviewed it and marked it for cleanup, not a state change. Monitoring for stale toggles that remain in production long past their expected removal date is a useful hygiene signal, not a health alarm.
Environment-level enable vs strategy-level enable
A toggle can be enabled at the environment level (env.enabled = true) but have no active strategies — in which case it evaluates to disabled for all users. Conversely, a toggle can have the default strategy (evaluates true for everyone) but have environment targeting off — in which case it also evaluates to disabled. Both conditions need checking independently.

Related guides