Guide · Feature Flags
MCP Tools for Split.io — Treatments, traffic allocation, dynamic config, impression tracking, and health monitoring
Split.io uses treatments — named strings (like "on", "off", or "control") — rather than boolean flags as its primary evaluation result. The special treatment "control" is returned when the SDK is not ready, the split definition is missing, or evaluation encounters an error — it is not a configured variation. MCP tools that treat "control" as equivalent to "off" mask critical SDK health problems. A split returning "control" for 100% of evaluations means the SDK's synchronization with Split.io's servers has failed, not that the feature is in its off state.
TL;DR
SDK: npm install @splitsoftware/splitio. Init: const factory = SplitFactory({ core: { authorizationKey: SDK_KEY } }); const client = factory.client(). Wait ready: await new Promise(r => client.on(client.Event.SDK_READY, r)). Evaluate: client.getTreatment(key, splitName). Config: client.getTreatmentWithConfig(key, splitName) → {treatment, config}. REST API: https://api.split.io/internal/api/v2/ with Authorization: Bearer <admin-api-key>. Health signal: SDK_READY event + SDK_READY_TIMED_OUT event (fired at 1.5s by default if not ready). The treatment "control" always means SDK or split error — never a configured variation.
Split.io architecture for MCP tool authors
Split.io's evaluation model centers on four concepts: splits (feature flags — named entities that define traffic allocations and treatments), segments (named groups of user keys for targeted rollouts), treatments (named string outcomes of evaluating a split for a user key), and dynamic configurations (JSON strings attached to each treatment for configuration delivery alongside the treatment string).
Two credential types control different operations:
- SDK API key: used by the Node.js SDK to synchronize split definitions and segment memberships from Split.io's servers over a streaming connection. Client-side keys (browser) are separate from server-side keys. Never use a client-side key in an MCP server — it has reduced permissions and different behavior.
- Admin API key: used for the REST API v2 — creating splits, managing traffic types, reading workspaces, viewing impressions. Higher privilege than SDK keys. Stored as a secret.
import SplitFactory from '@splitsoftware/splitio';
// Module-level factory singleton — one factory per SDK key
let splitFactory = null;
let splitClient = null;
async function getSplitClient(sdkKey, options = {}) {
if (splitClient !== null) return splitClient;
splitFactory = SplitFactory({
core: {
authorizationKey: sdkKey,
// key: 'default-key', // optional: set a default key for all evaluations
},
startup: {
readyTimeout: 5, // seconds to wait for SDK_READY (default 1.5)
},
impressionListener: {
logImpression(impressionData) {
// Log every evaluation for audit trail
// impressionData: {impression, attributes, sdkLanguageVersion, instance}
console.error(JSON.stringify({
event: 'split_impression',
feature: impressionData.impression.feature,
key: impressionData.impression.keyName,
treatment: impressionData.impression.treatment,
label: impressionData.impression.label, // reason for treatment
changeNumber: impressionData.impression.changeNumber,
ts: new Date().toISOString(),
}));
},
},
...options,
});
splitClient = splitFactory.client();
// Wait for SDK synchronization before returning
await new Promise((resolve, reject) => {
splitClient.on(splitClient.Event.SDK_READY, resolve);
splitClient.on(splitClient.Event.SDK_READY_TIMED_OUT, () => {
reject(new Error('Split SDK timed out waiting for split definitions'));
});
});
return splitClient;
}
// Admin REST API client
async function splitAdminApi(adminApiKey, path, method = 'GET', body = null) {
const res = await fetch(`https://api.split.io/internal/api/v2${path}`, {
method,
headers: {
'Authorization': `Bearer ${adminApiKey}`,
'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(`Split Admin API ${method} ${path} → ${res.status}: ${text.slice(0, 200)}`);
}
return method === 'DELETE' ? null : res.json();
}
Treatment evaluation with impression labels
Split.io's getTreatment() returns a treatment string. The label field (available via the impression listener) explains why that treatment was returned — it is the Split.io equivalent of an evaluation reason. Labels distinguish between "default rule" (fallthrough — no individual or segment rule matched), "in segment X" (segment targeting matched), "whitelisted" (individual allowlist), and "not in split" (user was excluded from traffic allocation).
// Evaluate a single split for a user key with attributes
async function getTreatment(sdkKey, splitName, key, attributes = {}) {
const client = await getSplitClient(sdkKey);
const treatment = client.getTreatment(key, splitName, attributes);
// "control" = SDK error or split doesn't exist — NEVER a valid configured treatment
if (treatment === 'control') {
// The impression listener will capture the label 'exception' or 'not ready'
return {
treatment: 'control',
error: true,
message: `"control" treatment: SDK not ready, split "${splitName}" not found, or evaluation error`,
};
}
return { treatment, error: false };
}
// Get treatment WITH dynamic configuration (for config delivery alongside feature flags)
async function getTreatmentWithConfig(sdkKey, splitName, key, attributes = {}) {
const client = await getSplitClient(sdkKey);
const result = client.getTreatmentWithConfig(key, splitName, attributes);
// result: { treatment: 'on', config: '{"maxRetries": 3}' } or { treatment: 'off', config: null }
if (result.treatment === 'control') {
return { treatment: 'control', config: null, error: true };
}
// config is a JSON string (or null) — parse to object for MCP tool return
let parsedConfig = null;
if (result.config !== null) {
try {
parsedConfig = JSON.parse(result.config);
} catch {
parsedConfig = result.config; // return raw string if not valid JSON
}
}
return { treatment: result.treatment, config: parsedConfig, error: false };
}
// Evaluate multiple splits in one batch
async function getTreatments(sdkKey, splitNames, key, attributes = {}) {
const client = await getSplitClient(sdkKey);
// getTreatments() evaluates all splits efficiently without N separate calls
const treatments = client.getTreatments(key, splitNames, attributes);
// treatments: { 'splitA': 'on', 'splitB': 'control', ... }
const results = {};
for (const [name, treatment] of Object.entries(treatments)) {
results[name] = {
treatment,
error: treatment === 'control',
};
}
return results;
}
Split definitions via Admin REST API
The Admin API v2 allows MCP tools to list splits, inspect their traffic allocations, read segment memberships, and create/archive splits programmatically. Split definitions live in a workspace and are associated with a traffic type — the entity type being targeted (e.g., "user", "organization", "device").
// List all splits in a workspace/environment
async function listSplits(adminApiKey, workspaceId, environmentId) {
const data = await splitAdminApi(
adminApiKey,
`/splits/ws/${workspaceId}?environmentId=${environmentId}&limit=100`
);
return (data.objects ?? []).map(split => ({
name: split.name,
trafficType: split.trafficType?.name,
description: split.description,
tags: split.tags ?? [],
createdAt: split.creationTime,
status: split.status, // ACTIVE | ARCHIVED
}));
}
// Get detailed split definition for one environment
async function getSplit(adminApiKey, workspaceId, splitName, environmentId) {
const split = await splitAdminApi(
adminApiKey,
`/splits/ws/${workspaceId}/${splitName}/environments/${environmentId}`
);
return {
name: split.name,
killed: split.killed, // killed = returns default treatment for all users
defaultTreatment: split.defaultTreatment,
treatments: split.treatments.map(t => ({
name: t.name,
configurations: t.configurations, // {JSON string of dynamic config | null}
description: t.description,
})),
trafficAllocation: split.trafficAllocation, // % of traffic receiving any non-default treatment
rules: split.rules, // targeting rules: segment rules, whitelist rules
defaultRule: split.defaultRule, // fallthrough treatment when no rules match
};
}
// List segments used for targeting
async function listSegments(adminApiKey, workspaceId, environmentId) {
const data = await splitAdminApi(
adminApiKey,
`/segments/ws/${workspaceId}?environmentId=${environmentId}`
);
return (data.objects ?? []).map(s => ({
name: s.name,
createdAt: s.creationTime,
description: s.description,
status: s.status,
}));
}
// Kill a split in an environment (returns default treatment for all users immediately)
async function killSplit(adminApiKey, workspaceId, splitName, environmentId) {
return splitAdminApi(
adminApiKey,
`/splits/ws/${workspaceId}/${splitName}/environments/${environmentId}/killed`,
'PUT',
{ killed: true }
);
}
The kill operation (killed: true) is Split.io's circuit-breaker pattern: when a split is killed, the SDK evaluates every user as receiving defaultTreatment regardless of targeting rules. This is a fast rollback mechanism — changes take effect on the SDK's next streaming update cycle (usually within 5 seconds). MCP tools that expose a kill_split tool must implement a confirm guard before calling this endpoint.
Impression listener for audit trail and experiment data
Impression data is the lifeblood of Split.io experiments: every getTreatment() call generates an impression, and those impressions drive the statistical engine that measures metric impacts across treatment groups. The impression label field explains why the treatment was assigned and is crucial for debugging unexpected treatment distributions.
// Impression labels and their meanings
const LABEL_MEANINGS = {
'killed': 'Split killed — all users get defaultTreatment',
'no rule matched': 'Fell through all rules — using defaultRule treatment',
'default rule': 'Fell through all rules — using defaultRule treatment (alias)',
'whitelisted': 'User is in individual whitelist for this treatment',
'in segment X': 'User matched segment rule for segment X',
'not in split': 'User excluded from traffic allocation (outside the X% slice)',
'not ready': 'SDK not initialized — returning control',
'exception': 'Error during evaluation — returning control',
};
// Custom impression aggregator for MCP tools
// (when impressionListener is too granular and you want summaries)
class ImpressionAggregator {
constructor() {
this.counts = new Map(); // {feature|treatment} -> count
}
logImpression({ impression }) {
const key = `${impression.feature}|${impression.treatment}`;
this.counts.set(key, (this.counts.get(key) ?? 0) + 1);
}
getSummary() {
const summary = {};
for (const [key, count] of this.counts) {
const [feature, treatment] = key.split('|');
if (!summary[feature]) summary[feature] = {};
summary[feature][treatment] = count;
}
return summary;
}
reset() {
this.counts.clear();
}
}
// Track a custom event (for metric impact measurement in experiments)
async function trackEvent(sdkKey, key, trafficType, eventType, value = null, properties = {}) {
const client = await getSplitClient(sdkKey);
const success = client.track(key, trafficType, eventType, value, properties);
// Returns false if the SDK is not ready — events are dropped, not buffered
if (!success) {
console.error(JSON.stringify({ event: 'split_track_failed', key, eventType, reason: 'not_ready' }));
}
return success;
}
Health check: SDK readiness and streaming connection
Split.io's SDK health has two independent dimensions: SDK readiness (has the SDK synchronized all split definitions and segment memberships from Split.io's servers?) and streaming connectivity (is the SDK receiving real-time split changes via the streaming connection?). A ready SDK can have a broken streaming connection — it will serve correct treatments from its cached definitions but will not pick up split changes until its next polling interval (default 60 seconds).
async function healthCheck(sdkKey, adminApiKey) {
const results = await Promise.allSettled([
// 1. SDK readiness — the core health signal
(async () => {
// Try to get client with short timeout for health probe
const clientOrTimeout = await Promise.race([
getSplitClient(sdkKey),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('SDK_READY timeout (5s)')), 5000)
),
]);
return { kind: 'sdk_ready', ready: true };
})(),
// 2. Synthetic evaluation — confirm evaluations return non-control values
// (for a known non-existent split, we expect 'control' with label 'exception' or not-found
// but no SDK-level error — this checks the evaluation path is operational)
(async () => {
const client = await getSplitClient(sdkKey);
const treatment = client.getTreatment('__health_probe_key__', '__alivemcp_probe_split__');
// 'control' is expected here (split doesn't exist) — that's healthy
// If this call throws, the SDK's evaluation engine has a problem
return {
kind: 'evaluation_path',
treatment,
healthy: true, // not throwing = evaluation path operational
};
})(),
// 3. Admin API access for management operations
...(adminApiKey ? [(async () => {
const workspaces = await splitAdminApi(adminApiKey, '/workspaces');
return {
kind: 'admin_api',
workspaceCount: (workspaces.objects ?? []).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 }
),
};
}
// SDK event handlers for ongoing health monitoring
function attachSdkEventHandlers(sdkKey) {
getSplitClient(sdkKey).then(client => {
client.on(client.Event.SDK_READY_TIMED_OUT, () => {
console.error(JSON.stringify({ event: 'split_sdk_timeout', ts: new Date().toISOString() }));
});
client.on(client.Event.SDK_UPDATE, () => {
// A split definition changed — log for change tracking
console.error(JSON.stringify({ event: 'split_sdk_update', ts: new Date().toISOString() }));
});
});
}
Set AliveMCP alert thresholds: critical when the SDK fires SDK_READY_TIMED_OUT or the Admin API returns 401 (admin key revoked); warning when the SDK has not received an SDK_UPDATE event in more than 10 minutes and a split change is known to have been made in that period (indicates broken streaming connection falling back to polling), or when more than 5% of treatments returned in a 1-minute window are "control" (indicates evaluation errors at scale).
Common integration pitfalls
- "control" is not a valid treatment — it always indicates an error
- Every configured split must have an explicit treatment list (e.g.,
["on", "off"])."control"is not in this list — it is the SDK's error sentinel. A split that evaluates to"control"for any user means either the SDK is not ready, the split name is misspelled, or evaluation encountered an exception. Never configure a real treatment named "control", and never treat"control"as equivalent to "off" in MCP tool logic. - Traffic allocation is not 100% by default
- A new split with one rule allocating 50% of users to "on" allocates the other 50% to... the
defaultTreatment, not to "off". Traffic allocation is the percentage of users receiving any non-default treatment. Users outside the allocation receivedefaultTreatmentregardless of rules. MCP tools reading traffic allocation must also surfacedefaultTreatmentto give a complete picture. - Segment membership changes require SDK restart in some SDK versions
- Segments are synchronized on SDK startup and updated via the streaming connection. In some SDK versions, adding a user key to a segment does not immediately affect treatment evaluation for a running SDK instance — the streaming update may arrive with a delay. For real-time segment membership changes, use the SDK_UPDATE event handler to log when definitions change, and probe evaluation after the event.
- track() returns false silently when SDK is not ready
- Unlike
getTreatment()which returns"control"on error,track()returnsfalseand drops the event when the SDK is not ready. Events are not buffered. If your MCP server callstrack()beforeSDK_READY, all early experiment events are silently lost, corrupting experiment data. Always calltrack()only after verifying SDK readiness.
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 OpenFeature — provider pattern, evaluation context, hooks, and typed flag evaluation
- MCP server health check patterns — composite endpoints and failure classification
- MCP server uptime monitoring — probes, intervals, and alert routing