Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 55 additions & 1 deletion PRD.md
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,57 @@ Returns a comprehensive markdown document containing:

---

## Phase 4: Strategy Constraints (Planned)

### Objective

Add `constraints` parameter to `set_flag_rollout` tool for targeting based on context fields.

### Scope

Extend `set_flag_rollout` to accept an optional `constraints` array passed through to the Unleash Admin API.

**Supported Context Fields** (Built-in):
- `userId`, `sessionId`, `remoteAddress`, `environment`, `appName`, `currentTime`, `properties`

**Note**: Custom context fields (e.g., `country`, `appVersion`) are supported but require Unleash Admin configuration.

### Input Schema

```typescript
interface StrategyConstraint {
contextName: string; // e.g., "userId", "appName"
operator: ConstraintOperator; // e.g., "IN", "NOT_IN", "SEMVER_GTE"
values?: string[]; // For IN/NOT_IN
value?: string; // For single value operators
}
```

**Operators**: `IN`, `NOT_IN`, `STR_CONTAINS`, `STR_STARTS_WITH`, `STR_ENDS_WITH`, `NUM_EQ/GT/GTE/LT/LTE`, `SEMVER_EQ/GT/GTE/LT/LTE`

### Example

```json
{
"featureName": "new-feature",
"environment": "production",
"rolloutPercentage": 100,
"constraints": [
{ "contextName": "userId", "operator": "IN", "values": ["user123", "user456"] }
]
}
```

### TODO: Phase 4 Tasks

- [ ] Add `StrategyConstraint` type to `src/unleash/client.ts`
- [ ] Update `setFlexibleRolloutStrategy` to include constraints in payload
- [ ] Add constraint schema to `src/tools/setFlagRollout.ts`
- [ ] Update README with examples
- [ ] Test against live Unleash instance

---

## Cross-Phase Tasks

### Documentation
Expand Down Expand Up @@ -451,6 +502,10 @@ Returns a comprehensive markdown document containing:
- [x] Users can copy-paste snippets directly into their codebase
- [x] Framework-specific templates for major frameworks

### Phase 4 (Planned)
- [ ] Constraints parameter working in `set_flag_rollout`
- [ ] Constraints correctly applied in Unleash dashboard

### Overall
- [x] LLM assistants can complete full flag workflow without human intervention
- [x] Average time to create and wrap a flag: <2 minutes
Expand Down Expand Up @@ -488,7 +543,6 @@ Returns a comprehensive markdown document containing:
- Tag assignment during creation
- Initial strategy configuration
- Variant creation
- Constraint configuration

---

Expand Down
58 changes: 58 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,64 @@ Returns a comprehensive, markdown-formatted string that guides the user on how t
[If-block, guard clause, hooks, ternary, etc.]
```

### Set flag rollout

The `set_flag_rollout` tool configures a `flexibleRollout` strategy for a feature flag in a specific environment. This includes rollout percentage, stickiness, variants, and constraints for targeting.

#### When to use

Use this tool after creating a feature flag when you want to configure how the flag rolls out to users. This tool does **not** enable the flag; use `toggle_flag_environment` to turn it on.

#### Parameters

- `featureName` (required): The feature flag name.
- `environment` (required): Target environment (e.g., `development`, `production`).
- `rolloutPercentage` (required): Percentage of users to receive the feature (0-100).
- `projectId` (optional): Project ID (defaults to `UNLEASH_DEFAULT_PROJECT`).
- `groupId` (optional): Group ID for stickiness bucketing (defaults to feature name).
- `stickiness` (optional): Stickiness field (defaults to `"default"`).
- `title` (optional): Descriptive title for the strategy.
- `disabled` (optional): Disable the strategy (defaults to `false`).
- `variants` (optional): List of strategy-level variants.
- `constraints` (optional): List of targeting constraints.

#### Constraints

Constraints allow you to target specific users based on context fields. Each constraint has:

- `contextName` (required): Context field name (e.g., `userId`, `environment`).
- `operator` (required): Comparison operator.
- `values` (optional): Values for multi-value operators (`IN`, `NOT_IN`).
- `value` (optional): Single value for single-value operators.
- `caseInsensitive` (optional): Case insensitive matching.
- `inverted` (optional): Invert the constraint result.

**Available operators:**
- Basic: `IN`, `NOT_IN`
- String: `STR_CONTAINS`, `STR_STARTS_WITH`, `STR_ENDS_WITH`
- Numeric: `NUM_EQ`, `NUM_GT`, `NUM_GTE`, `NUM_LT`, `NUM_LTE`
- Date: `DATE_AFTER`, `DATE_BEFORE`
- Semver: `SEMVER_EQ`, `SEMVER_GT`, `SEMVER_LT`

#### Usage example

**Tool payload**

```json
{
"featureName": "new-checkout-flow",
"environment": "production",
"rolloutPercentage": 50,
"constraints": [
{
"contextName": "userId",
"operator": "IN",
"values": ["user-123", "user-456"]
}
]
}
```

## Architecture

The server follows a focused, purpose-driven design.
Expand Down
51 changes: 50 additions & 1 deletion src/tools/setFlagRollout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ import {
resolveProjectId,
type ServerContext,
} from '../context.js';
import type { StrategyVariant, StrategyVariantPayload } from '../unleash/client.js';
import type {
StrategyConstraint,
StrategyVariant,
StrategyVariantPayload,
} from '../unleash/client.js';
import { createFlagResourceLink } from '../utils/streaming.js';

const variantPayloadSchema = z.object({
Expand All @@ -28,6 +32,35 @@ const variantSchema = z
})
.describe('Strategy-level variant definition');

const constraintOperatorSchema = z.enum([
'IN',
'NOT_IN',
'STR_CONTAINS',
'STR_STARTS_WITH',
'STR_ENDS_WITH',
'NUM_EQ',
'NUM_GT',
'NUM_GTE',
'NUM_LT',
'NUM_LTE',
'DATE_AFTER',
'DATE_BEFORE',
'SEMVER_EQ',
'SEMVER_GT',
'SEMVER_LT',
]);

const constraintSchema = z
.object({
contextName: z.string().min(1).describe('Context field name (e.g., userId, environment)'),
operator: constraintOperatorSchema.describe('Comparison operator'),
values: z.array(z.string()).optional().describe('Values for multi-value operators (IN, NOT_IN)'),
value: z.string().optional().describe('Single value for single-value operators'),
caseInsensitive: z.boolean().optional().describe('Case insensitive matching (default: false)'),
inverted: z.boolean().optional().describe('Invert the constraint result (default: false)'),
})
.describe('Strategy constraint for targeting');

const setFlagRolloutSchema = z.object({
projectId: z
.string()
Expand All @@ -46,6 +79,10 @@ const setFlagRolloutSchema = z.object({
title: z.string().optional().describe('Optional descriptive title for the strategy'),
disabled: z.boolean().optional().describe('Disable the strategy (defaults to false)'),
variants: z.array(variantSchema).optional().describe('Optional list of strategy-level variants'),
constraints: z
.array(constraintSchema)
.optional()
.describe('Optional list of strategy constraints for targeting'),
});

type SetFlagRolloutInput = z.infer<typeof setFlagRolloutSchema>;
Expand Down Expand Up @@ -79,6 +116,17 @@ export async function setFlagRollout(
...(variant.payload ? { payload: variant.payload } : {}),
}));

const constraints: StrategyConstraint[] | undefined = input.constraints?.map((constraint) => ({
contextName: constraint.contextName,
operator: constraint.operator,
...(constraint.values ? { values: constraint.values } : {}),
...(constraint.value ? { value: constraint.value } : {}),
...(constraint.caseInsensitive !== undefined
? { caseInsensitive: constraint.caseInsensitive }
: {}),
...(constraint.inverted !== undefined ? { inverted: constraint.inverted } : {}),
}));

const strategy = await context.unleashClient.setFlexibleRolloutStrategy(
projectId,
input.featureName,
Expand All @@ -90,6 +138,7 @@ export async function setFlagRollout(
title: input.title,
disabled: input.disabled,
variants,
constraints,
},
);

Expand Down
41 changes: 40 additions & 1 deletion src/unleash/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,48 @@ export interface StrategyVariant {
[key: string]: unknown;
}

/**
* Constraint operators supported by Unleash.
* See: https://docs.getunleash.io/reference/activation-strategies
*/
export type ConstraintOperator =
| 'IN'
| 'NOT_IN'
| 'STR_CONTAINS'
| 'STR_STARTS_WITH'
| 'STR_ENDS_WITH'
| 'NUM_EQ'
| 'NUM_GT'
| 'NUM_GTE'
| 'NUM_LT'
| 'NUM_LTE'
| 'DATE_AFTER'
| 'DATE_BEFORE'
| 'SEMVER_EQ'
| 'SEMVER_GT'
| 'SEMVER_LT';

/**
* Strategy constraint for targeting.
* See: https://docs.getunleash.io/reference/activation-strategies
*/
export interface StrategyConstraint {
contextName: string;
operator: ConstraintOperator;
values?: string[];
value?: string;
caseInsensitive?: boolean;
inverted?: boolean;
}

export interface SetFlagRolloutOptions {
rolloutPercentage: number;
groupId?: string;
stickiness?: string;
title?: string;
disabled?: boolean;
variants?: StrategyVariant[];
constraints?: StrategyConstraint[];
}

export interface FeatureStrategy {
Expand All @@ -86,7 +121,7 @@ export interface FeatureStrategy {
featureName?: string;
sortOrder?: number;
segments?: number[];
constraints?: Array<Record<string, unknown>>;
constraints?: StrategyConstraint[];
variants?: StrategyVariant[];
parameters: Record<string, string>;
}
Expand Down Expand Up @@ -265,6 +300,9 @@ export class UnleashClient {
disabled: options.disabled,
parameters,
...(options.variants && options.variants.length > 0 ? { variants: options.variants } : {}),
...(options.constraints && options.constraints.length > 0
? { constraints: options.constraints }
: {}),
};

if (this.dryRun) {
Expand All @@ -276,6 +314,7 @@ export class UnleashClient {
featureName,
parameters,
variants: payload.variants ?? [],
constraints: payload.constraints ?? [],
};
}

Expand Down
Loading