@featureflip/mcp 0.1.11 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.mjs +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/generated/api-types.d.ts +386 -7
- package/dist/generated/api-types.d.ts.map +1 -1
- package/dist/generated/output-schemas.json.d.ts +2964 -0
- package/dist/index.mjs +1 -2
- package/dist/org-D-y_4-WG.js +1357 -0
- package/dist/tools/environments.d.ts.map +1 -1
- package/dist/tools/flag-environments.d.ts.map +1 -1
- package/dist/tools/flags.d.ts.map +1 -1
- package/dist/tools/outputs.d.ts +18 -0
- package/dist/tools/outputs.d.ts.map +1 -0
- package/dist/tools/params.d.ts +9 -0
- package/dist/tools/params.d.ts.map +1 -0
- package/dist/tools/projects.d.ts.map +1 -1
- package/dist/tools/segments.d.ts.map +1 -1
- package/dist/tools/targeting.d.ts.map +1 -1
- package/dist/tools/variations.d.ts.map +1 -1
- package/dist/tools/webhooks.d.ts.map +1 -1
- package/dist/tools/workflows/stale-flags.d.ts.map +1 -1
- package/dist/tools/workflows/wrap-feature.d.ts.map +1 -1
- package/package.json +5 -4
- package/dist/org-c4iWMSzU.js +0 -1033
|
@@ -0,0 +1,1357 @@
|
|
|
1
|
+
import { McpServer as e, fromJsonSchema as t } from "@modelcontextprotocol/server";
|
|
2
|
+
import * as n from "zod/v4";
|
|
3
|
+
//#endregion
|
|
4
|
+
//#region src/version.ts
|
|
5
|
+
var r = {
|
|
6
|
+
name: "@featureflip/mcp",
|
|
7
|
+
version: "0.1.13",
|
|
8
|
+
mcpName: "io.github.canopy-labs/featureflip",
|
|
9
|
+
description: "MCP server for managing Featureflip feature flags from AI agents and editors",
|
|
10
|
+
keywords: [
|
|
11
|
+
"feature-flags",
|
|
12
|
+
"feature-toggles",
|
|
13
|
+
"feature-management",
|
|
14
|
+
"featureflip",
|
|
15
|
+
"mcp",
|
|
16
|
+
"mcp-server",
|
|
17
|
+
"model-context-protocol",
|
|
18
|
+
"ai-agents",
|
|
19
|
+
"claude",
|
|
20
|
+
"cursor"
|
|
21
|
+
],
|
|
22
|
+
license: "Apache-2.0",
|
|
23
|
+
repository: {
|
|
24
|
+
type: "git",
|
|
25
|
+
url: "https://github.com/canopy-labs/featureflip-mcp.git"
|
|
26
|
+
},
|
|
27
|
+
homepage: "https://featureflip.io/docs/integrations/mcp/",
|
|
28
|
+
type: "module",
|
|
29
|
+
bin: { "featureflip-mcp": "bin/featureflip-mcp.mjs" },
|
|
30
|
+
exports: { ".": {
|
|
31
|
+
types: "./dist/index.d.ts",
|
|
32
|
+
import: "./dist/index.mjs"
|
|
33
|
+
} },
|
|
34
|
+
types: "./dist/index.d.ts",
|
|
35
|
+
files: ["dist", "bin"],
|
|
36
|
+
publishConfig: { access: "public" },
|
|
37
|
+
scripts: {
|
|
38
|
+
build: "vite build",
|
|
39
|
+
typecheck: "tsc --noEmit -p tsconfig.json",
|
|
40
|
+
"generate:api": "npx --yes openapi-typescript@7.13.0 ../../apps/management-api/public-v1-openapi.json -o src/generated/api-types.ts && node scripts/generate-output-schemas.mjs",
|
|
41
|
+
test: "vitest run",
|
|
42
|
+
"test:watch": "vitest",
|
|
43
|
+
"smoke:docker": "node scripts/docker-smoke.mjs",
|
|
44
|
+
"build:mcpb": "node scripts/build-mcpb.mjs"
|
|
45
|
+
},
|
|
46
|
+
dependencies: {
|
|
47
|
+
"@modelcontextprotocol/server": "2.3.1",
|
|
48
|
+
zod: "^4.6.5"
|
|
49
|
+
},
|
|
50
|
+
devDependencies: {
|
|
51
|
+
"@modelcontextprotocol/client": "2.3.1",
|
|
52
|
+
"@typescript/typescript6": "^6.0.2",
|
|
53
|
+
"@types/node": "^26.6.4",
|
|
54
|
+
typescript: "^7.0.2",
|
|
55
|
+
vite: "^8.3.4",
|
|
56
|
+
"vite-plugin-dts": "^5.1.2",
|
|
57
|
+
vitest: "^5.0.3"
|
|
58
|
+
},
|
|
59
|
+
engines: { node: ">=20.19.0" }
|
|
60
|
+
}.version, i = class extends Error {
|
|
61
|
+
status;
|
|
62
|
+
envelope;
|
|
63
|
+
constructor(e, t) {
|
|
64
|
+
super(t.message), this.status = e, this.envelope = t, this.name = "ApiError";
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
function a(e) {
|
|
68
|
+
return encodeURIComponent(e);
|
|
69
|
+
}
|
|
70
|
+
var o = /* @__PURE__ */ new Set([
|
|
71
|
+
502,
|
|
72
|
+
503,
|
|
73
|
+
504
|
|
74
|
+
]), s = /* @__PURE__ */ new Set([
|
|
75
|
+
"GET",
|
|
76
|
+
"HEAD",
|
|
77
|
+
"PUT",
|
|
78
|
+
"DELETE"
|
|
79
|
+
]), c = [250, 1e3], l = class {
|
|
80
|
+
opts;
|
|
81
|
+
fetchImpl;
|
|
82
|
+
sleep;
|
|
83
|
+
constructor(e) {
|
|
84
|
+
this.opts = e, this.fetchImpl = e.fetchImpl ?? fetch, this.sleep = e.sleep ?? ((e) => new Promise((t) => setTimeout(t, e)));
|
|
85
|
+
}
|
|
86
|
+
async request(e, t, n = {}) {
|
|
87
|
+
let r = new URL(this.opts.baseUrl + t);
|
|
88
|
+
for (let [e, t] of Object.entries(n.query ?? {})) t !== void 0 && r.searchParams.set(e, String(t));
|
|
89
|
+
let a = {
|
|
90
|
+
Authorization: `Bearer ${this.opts.token}`,
|
|
91
|
+
Accept: "application/json"
|
|
92
|
+
}, l;
|
|
93
|
+
n.body !== void 0 && (a["Content-Type"] = "application/json", l = JSON.stringify(n.body)), n.idempotencyKey && (a["Idempotency-Key"] = n.idempotencyKey);
|
|
94
|
+
let u = s.has(e) || n.idempotencyKey !== void 0, d;
|
|
95
|
+
for (let t = 0;; t++) {
|
|
96
|
+
let n = t < c.length;
|
|
97
|
+
try {
|
|
98
|
+
d = await this.fetchImpl(r.toString(), {
|
|
99
|
+
method: e,
|
|
100
|
+
headers: a,
|
|
101
|
+
body: l
|
|
102
|
+
});
|
|
103
|
+
} catch (e) {
|
|
104
|
+
if (!u || !n) throw e;
|
|
105
|
+
await this.sleep(c[t]);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (!(u ? o.has(d.status) : d.status === 503) || !n) break;
|
|
109
|
+
await d.body?.cancel(), await this.sleep(c[t]);
|
|
110
|
+
}
|
|
111
|
+
if (d.status !== 204) {
|
|
112
|
+
if (!d.ok) {
|
|
113
|
+
let e;
|
|
114
|
+
try {
|
|
115
|
+
if (e = await d.json(), typeof e?.error != "string") throw Error("not an envelope");
|
|
116
|
+
} catch {
|
|
117
|
+
e = {
|
|
118
|
+
error: `http_${d.status}`,
|
|
119
|
+
message: `HTTP ${d.status} from ${r.pathname}`
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
throw new i(d.status, e);
|
|
123
|
+
}
|
|
124
|
+
return await d.json();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
//#endregion
|
|
129
|
+
//#region src/errors.ts
|
|
130
|
+
function u(e) {
|
|
131
|
+
let t = [{
|
|
132
|
+
type: "text",
|
|
133
|
+
text: JSON.stringify(e, null, 2)
|
|
134
|
+
}];
|
|
135
|
+
return typeof e == "object" && e && !Array.isArray(e) ? {
|
|
136
|
+
content: t,
|
|
137
|
+
structuredContent: e
|
|
138
|
+
} : { content: t };
|
|
139
|
+
}
|
|
140
|
+
function d(e) {
|
|
141
|
+
if (e instanceof i) {
|
|
142
|
+
let t = e.envelope, n = [`Featureflip API error ${e.status} (${t.error}): ${t.message}`];
|
|
143
|
+
return t.fields && n.push(`Field errors: ${JSON.stringify(t.fields)}`), t.did_you_mean?.length && n.push(`Did you mean: ${t.did_you_mean.join(", ")}`), t.next_actions?.length && n.push(`Next actions: ${t.next_actions.map((e) => `${e.method} ${e.path}`).join("; ")}`), t.retry_after != null && n.push(`Rate limited — retry after ${t.retry_after}s.`), e.status === 404 && n.push("Note: 404 can also mean the public Management API is not enabled for this organization yet, or a service token does not include this project."), t.docs_url && n.push(`Docs: ${t.docs_url}`), {
|
|
144
|
+
content: [{
|
|
145
|
+
type: "text",
|
|
146
|
+
text: n.join("\n")
|
|
147
|
+
}],
|
|
148
|
+
isError: !0
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
content: [{
|
|
153
|
+
type: "text",
|
|
154
|
+
text: `Error: ${e instanceof Error ? e.message : String(e)}`
|
|
155
|
+
}],
|
|
156
|
+
isError: !0
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
async function f(e) {
|
|
160
|
+
try {
|
|
161
|
+
return await e();
|
|
162
|
+
} catch (e) {
|
|
163
|
+
return d(e);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
var p = {
|
|
167
|
+
endpoints: {
|
|
168
|
+
"POST /api/v1/orgs/{org}/projects/{project}/environments": "PublicEnvironmentResponse",
|
|
169
|
+
"GET /api/v1/orgs/{org}/projects/{project}/environments": "PublicEnvironmentResponsePagedResult",
|
|
170
|
+
"GET /api/v1/orgs/{org}/projects/{project}/environments/{env}": "PublicEnvironmentResponse",
|
|
171
|
+
"POST /api/v1/orgs/{org}/projects/{project}/flags": "PublicFlagResponse",
|
|
172
|
+
"GET /api/v1/orgs/{org}/projects/{project}/flags": "PublicFlagListItemPagedResult",
|
|
173
|
+
"GET /api/v1/orgs/{org}/projects/{project}/flags/removal-candidates": "PublicFlagRemovalCandidatePagedResult",
|
|
174
|
+
"GET /api/v1/orgs/{org}/projects/{project}/flags/{flag}": "PublicFlagResponse",
|
|
175
|
+
"GET /api/v1/orgs/{org}/projects/{project}/flags/{flag}/environments/{env}": "PublicFlagEnvConfigResponse",
|
|
176
|
+
"GET /api/v1/orgs/{org}/projects/{project}/flags/{flag}/environments/{env}/targeting": "PublicTargetingConfigResponse",
|
|
177
|
+
"POST /api/v1/orgs/{org}/projects/{project}/flags/{flag}/environments/{env}/rules": "PublicTargetingRuleResponse",
|
|
178
|
+
"GET /api/v1/orgs/{org}/projects/{project}/flags/{flag}/environments/{env}/rules/{ruleId}": "PublicTargetingRuleResponse",
|
|
179
|
+
"POST /api/v1/orgs/{org}/projects/{project}/flags/{flag}/variations": "PublicVariationResponse",
|
|
180
|
+
"GET /api/v1/me": "MeResponse",
|
|
181
|
+
"GET /api/v1/orgs": "OrganizationResponsePagedResult",
|
|
182
|
+
"GET /api/v1/orgs/{org}": "OrganizationResponse",
|
|
183
|
+
"POST /api/v1/orgs/{org}/projects": "ProjectResponse",
|
|
184
|
+
"GET /api/v1/orgs/{org}/projects": "ProjectResponsePagedResult",
|
|
185
|
+
"GET /api/v1/orgs/{org}/projects/{project}": "ProjectResponse",
|
|
186
|
+
"GET /api/v1/orgs/{org}/projects/{project}/default-flag-lifetime/impact": "PublicDefaultFlagLifetimeImpact",
|
|
187
|
+
"POST /api/v1/orgs/{org}/projects/{project}/environments/{env}/sdk-keys": "PublicCreateSdkKeyResponse",
|
|
188
|
+
"GET /api/v1/orgs/{org}/projects/{project}/environments/{env}/sdk-keys": "PublicSdkKeyResponsePagedResult",
|
|
189
|
+
"GET /api/v1/orgs/{org}/projects/{project}/environments/{env}/sdk-keys/{sdkKey}": "PublicSdkKeyResponse",
|
|
190
|
+
"POST /api/v1/orgs/{org}/projects/{project}/segments": "PublicSegmentResponse",
|
|
191
|
+
"GET /api/v1/orgs/{org}/projects/{project}/segments": "PublicSegmentResponsePagedResult",
|
|
192
|
+
"GET /api/v1/orgs/{org}/projects/{project}/segments/{segment}": "PublicSegmentResponse",
|
|
193
|
+
"GET /api/v1/orgs/{org}/webhooks": "PublicWebhookSubscriptionResponsePagedResult",
|
|
194
|
+
"POST /api/v1/orgs/{org}/webhooks": "PublicCreateWebhookSubscriptionResponse",
|
|
195
|
+
"GET /api/v1/orgs/{org}/webhooks/{id}": "PublicWebhookSubscriptionResponse",
|
|
196
|
+
"POST /api/v1/orgs/{org}/webhooks/{id}/secrets": "PublicRotateWebhookSecretResponse",
|
|
197
|
+
"POST /api/v1/orgs/{org}/webhooks/{id}/test": "PublicTestWebhookSubscriptionResponse",
|
|
198
|
+
"GET /api/v1/orgs/{org}/webhooks/{id}/deliveries": "PublicWebhookDeliveryResponsePagedResult"
|
|
199
|
+
},
|
|
200
|
+
$defs: /* @__PURE__ */ JSON.parse("{\"ActionCapability\":{\"type\":[\"object\",\"null\"],\"properties\":{\"allowed\":{\"type\":[\"boolean\",\"null\"]},\"reason\":{\"type\":[\"string\",\"null\"]}},\"description\":\"One entry in a resource's `_actions` block: may the caller perform it, and if not, why.\"},\"MeResponse\":{\"type\":[\"object\",\"null\"],\"properties\":{\"type\":{\"type\":[\"string\",\"null\"]},\"id\":{\"type\":[\"string\",\"null\"]},\"name\":{\"type\":[\"string\",\"null\"]},\"email\":{\"type\":[\"string\",\"null\"]},\"emailVerified\":{\"type\":[\"boolean\",\"null\"]},\"organizationId\":{\"type\":[\"string\",\"null\"]},\"role\":{\"type\":[\"string\",\"null\"]},\"projectScope\":{\"type\":[\"array\",\"null\"],\"items\":{\"type\":[\"string\",\"null\"]}}},\"description\":\"The authenticated caller's identity, returned by `GET /api/v1/me`. The public API\\nauthenticates two kinds of principal, so this is a `type`-discriminated shape.\\nA `\\\"user\\\"` caller (a session/JWT user or a personal access token, `ffp_...`) carries\\n`email` + `emailVerified`. A `\\\"service_token\\\"` caller (`ffs_...`) has no\\nbacking user row, so it carries `organizationId` + `role` + `projectScope` (the\\ntoken's org, its role in that org, and its project allowlist) instead. The type-inapplicable\\nfields are omitted from the response body. `projectScope` is omitted for an unrestricted\\nservice token (access to every project), never an empty list.\"},\"NextAction\":{\"type\":[\"object\",\"null\"],\"properties\":{\"method\":{\"type\":[\"string\",\"null\"]},\"path\":{\"type\":[\"string\",\"null\"]}}},\"OrganizationResponse\":{\"type\":[\"object\",\"null\"],\"properties\":{\"id\":{\"type\":[\"string\",\"null\"]},\"name\":{\"type\":[\"string\",\"null\"]},\"slug\":{\"type\":[\"string\",\"null\"]},\"plan\":{\"type\":[\"string\",\"null\"],\"description\":\"Allowed values: Solo, Team, Pro, Business, Enterprise.\"},\"isActive\":{\"type\":[\"boolean\",\"null\"]},\"createdAt\":{\"type\":[\"string\",\"null\"]}},\"description\":\"Public representation of an organization, returned by both `GET /api/v1/orgs` (list) and\\n`GET /api/v1/orgs/{org}` (detail). Drops dashboard-only fields (`BillingEmail` is\\nbilling-contact PII, `UpdatedAt` is dashboard audit trivia) from the dashboard's\\nOrganizationDto/OrganizationListItemDto.\"},\"OrganizationResponsePagedResult\":{\"type\":[\"object\",\"null\"],\"properties\":{\"items\":{\"type\":[\"array\",\"null\"],\"items\":{\"anyOf\":[{\"$ref\":\"#/$defs/OrganizationResponse\"},{\"type\":\"null\"}]}},\"next_cursor\":{\"type\":[\"string\",\"null\"]},\"_actions\":{\"type\":[\"object\",\"null\"],\"additionalProperties\":{\"anyOf\":[{\"$ref\":\"#/$defs/ActionCapability\"},{\"type\":\"null\"}]},\"description\":\"Capability hints: which operations the authenticated caller may perform on this resource, each with allowed + optional reason. Computed from the caller's role and the resource's state. Injected at runtime; safe to ignore. Present only on top-level resource responses — nested occurrences (e.g. a rule inside a targeting-config response) do not carry it.\"}},\"description\":\"A page of results plus an opaque cursor for the next page, or null when this is the last\\npage. Wire keys are frozen: `items` (already lowercase under the camelCase policy) and\\n`next_cursor` (pinned explicitly — the policy alone would emit `nextCursor`).\"},\"ProjectResponse\":{\"type\":[\"object\",\"null\"],\"properties\":{\"id\":{\"type\":[\"string\",\"null\"]},\"key\":{\"type\":[\"string\",\"null\"]},\"name\":{\"type\":[\"string\",\"null\"]},\"description\":{\"type\":[\"string\",\"null\"]},\"createdAt\":{\"type\":[\"string\",\"null\"]},\"updatedAt\":{\"type\":[\"string\",\"null\"]},\"defaultFlagLifetimeDays\":{\"type\":[\"integer\",\"null\"]},\"_actions\":{\"type\":[\"object\",\"null\"],\"additionalProperties\":{\"anyOf\":[{\"$ref\":\"#/$defs/ActionCapability\"},{\"type\":\"null\"}]},\"description\":\"Capability hints: which operations the authenticated caller may perform on this resource, each with allowed + optional reason. Computed from the caller's role and the resource's state. Injected at runtime; safe to ignore. Present only on top-level resource responses — nested occurrences (e.g. a rule inside a targeting-config response) do not carry it.\"}},\"description\":\"Public representation of a project, returned by `GET /api/v1/orgs/{org}/projects` (list),\\n`GET .../projects/{project}` (detail), and the body of a successful create/update. Drops\\nthe dashboard-only nested `Environments` collection from ProjectDto — the\\npublic API surfaces environments via their own endpoint (Task 6), not embedded here.\\nUpdatedAt is only populated from ProjectDto (the detail/create\\nshape); ProjectListItemDto doesn't carry it, so list rows leave it `null`.\\nDefaultFlagLifetimeDays is the project's default flag lifetime in days, or null (#3194).\"},\"ProjectResponsePagedResult\":{\"type\":[\"object\",\"null\"],\"properties\":{\"items\":{\"type\":[\"array\",\"null\"],\"items\":{\"anyOf\":[{\"$ref\":\"#/$defs/ProjectResponse\"},{\"type\":\"null\"}]}},\"next_cursor\":{\"type\":[\"string\",\"null\"]},\"_actions\":{\"type\":[\"object\",\"null\"],\"additionalProperties\":{\"anyOf\":[{\"$ref\":\"#/$defs/ActionCapability\"},{\"type\":\"null\"}]},\"description\":\"Capability hints: which operations the authenticated caller may perform on this resource, each with allowed + optional reason. Computed from the caller's role and the resource's state. Injected at runtime; safe to ignore. Present only on top-level resource responses — nested occurrences (e.g. a rule inside a targeting-config response) do not carry it.\"}},\"description\":\"A page of results plus an opaque cursor for the next page, or null when this is the last\\npage. Wire keys are frozen: `items` (already lowercase under the camelCase policy) and\\n`next_cursor` (pinned explicitly — the policy alone would emit `nextCursor`).\"},\"PublicAddVariationRequest\":{\"type\":[\"object\",\"null\"],\"properties\":{\"key\":{\"type\":[\"string\",\"null\"]},\"name\":{\"type\":[\"string\",\"null\"]},\"value\":{\"type\":[\"string\",\"null\"]},\"description\":{\"type\":[\"string\",\"null\"]}},\"description\":\"Request body for\\n`POST /api/v1/orgs/{org}/projects/{project}/flags/{flag}/variations`. Named distinctly\\nfrom the dashboard's own `AddVariationRequest` —\\nsee PublicCreateEnvironmentRequest for the schemaId-collision rationale (both\\ncontrollers share one Swagger document).\"},\"PublicApiErrorEnvelope\":{\"type\":[\"object\",\"null\"],\"properties\":{\"error\":{\"type\":[\"string\",\"null\"]},\"message\":{\"type\":[\"string\",\"null\"]},\"docs_url\":{\"type\":[\"string\",\"null\"]},\"fields\":{\"type\":[\"object\",\"null\"],\"additionalProperties\":{\"type\":[\"array\",\"null\"],\"items\":{\"type\":[\"string\",\"null\"]}}},\"retry_after\":{\"type\":[\"integer\",\"null\"]},\"did_you_mean\":{\"type\":[\"array\",\"null\"],\"items\":{\"type\":[\"string\",\"null\"]}},\"next_actions\":{\"type\":[\"array\",\"null\"],\"items\":{\"anyOf\":[{\"$ref\":\"#/$defs/NextAction\"},{\"type\":\"null\"}]}},\"connection_id\":{\"type\":[\"string\",\"null\"]}},\"description\":\"The frozen public-API error contract. `error` codes are stable snake_case strings. Wire keys\\nare frozen snake_case too (`error`, `message`, `docs_url`, `fields`,\\n`retry_after`) — the global camelCase naming policy would otherwise emit\\n`docsUrl`/`retryAfter`, breaking the published spec. !:JsonPropertyName\\nalways wins over the policy, so these are pinned explicitly rather than relying on the\\nproperty names already being lowercase for the single-word ones.\\n`did_you_mean`, `next_actions` and `connection_id` are ADDITIVE optional keys\\n(null → omitted via the global `DefaultIgnoreCondition = WhenWritingNull`), so\\npre-existing error bodies are byte-for-byte unchanged when they're absent.\\n\\n`connection_id` is present only on the `sso_required` error: the organization\\nrequires single sign-on, and this credential didn't come through its identity provider.\\n`connection_id` identifies which SSO connection to sign in with.\"},\"PublicConditionGroupRequest\":{\"type\":[\"object\",\"null\"],\"properties\":{\"operator\":{\"type\":[\"string\",\"null\"],\"description\":\"Allowed values: And, Or.\"},\"conditions\":{\"type\":[\"array\",\"null\"],\"items\":{\"anyOf\":[{\"$ref\":\"#/$defs/PublicConditionRequest\"},{\"type\":\"null\"}]}}},\"description\":\"Request-side condition group, symmetric with PublicConditionGroupResponse.\"},\"PublicConditionGroupResponse\":{\"type\":[\"object\",\"null\"],\"properties\":{\"operator\":{\"type\":[\"string\",\"null\"],\"description\":\"Allowed values: And, Or.\"},\"conditions\":{\"type\":[\"array\",\"null\"],\"items\":{\"anyOf\":[{\"$ref\":\"#/$defs/PublicConditionResponse\"},{\"type\":\"null\"}]}}},\"description\":\"A condition group within a PublicTargetingRuleResponse. Maps 1:1 onto\\nConditionGroupDto. Operator is a bare wire string naming one of\\nthe `ConditionGroupOperator` members (`\\\"And\\\"`/`\\\"Or\\\"`) —\\ngroups are ANDed together at the rule level; within a group, its conditions combine using\\nthis operator.\"},\"PublicConditionRequest\":{\"type\":[\"object\",\"null\"],\"properties\":{\"attribute\":{\"type\":[\"string\",\"null\"]},\"operator\":{\"type\":[\"string\",\"null\"],\"description\":\"Allowed values: Equals, NotEquals, Contains, NotContains, GreaterThan, LessThan, GreaterThanOrEqual, LessThanOrEqual, In, NotIn, MatchesRegex, StartsWith, EndsWith, Before, After, SemverEquals, SemverGreaterThan, SemverGreaterThanOrEqual, SemverLessThan, SemverLessThanOrEqual.\"},\"values\":{\"type\":[\"array\",\"null\"],\"items\":{\"type\":[\"string\",\"null\"]}},\"negate\":{\"type\":[\"boolean\",\"null\"]}},\"description\":\"Request-side condition, symmetric with PublicConditionResponse.\"},\"PublicConditionResponse\":{\"type\":[\"object\",\"null\"],\"properties\":{\"attribute\":{\"type\":[\"string\",\"null\"]},\"operator\":{\"type\":[\"string\",\"null\"],\"description\":\"Allowed values: Equals, NotEquals, Contains, NotContains, GreaterThan, LessThan, GreaterThanOrEqual, LessThanOrEqual, In, NotIn, MatchesRegex, StartsWith, EndsWith, Before, After, SemverEquals, SemverGreaterThan, SemverGreaterThanOrEqual, SemverLessThan, SemverLessThanOrEqual.\"},\"values\":{\"type\":[\"array\",\"null\"],\"items\":{\"type\":[\"string\",\"null\"]}},\"negate\":{\"type\":[\"boolean\",\"null\"]}},\"description\":\"A single condition within a PublicConditionGroupResponse. Maps 1:1 onto\\nConditionDto. Operator is a bare wire string naming one of the\\n`OperatorType` members (e.g. `\\\"Equals\\\"`, `\\\"In\\\"`).\"},\"PublicCreateEnvironmentRequest\":{\"type\":[\"object\",\"null\"],\"properties\":{\"key\":{\"type\":[\"string\",\"null\"]},\"name\":{\"type\":[\"string\",\"null\"]},\"color\":{\"type\":[\"string\",\"null\"]},\"sortOrder\":{\"type\":[\"integer\",\"null\"]}},\"description\":\"Request body for `POST /api/v1/orgs/{org}/projects/{project}/environments`. Named\\ndistinctly from the dashboard's own `CreateEnvironmentRequest`\\n— both controllers share one Swagger document (see `Program.cs`'s `AddSwaggerGen``DocInclusionPredicate` override), and Swashbuckle's default schemaId is the bare type\\nname, so an identical name in either namespace throws a schemaId collision at\\n`--export-openapi` time.\"},\"PublicCreateFeatureFlagRequest\":{\"type\":[\"object\",\"null\"],\"properties\":{\"key\":{\"type\":[\"string\",\"null\"]},\"name\":{\"type\":[\"string\",\"null\"]},\"type\":{\"type\":[\"string\",\"null\"],\"description\":\"Allowed values: Boolean, String, Number, Json.\"},\"description\":{\"type\":[\"string\",\"null\"]},\"tags\":{\"type\":[\"array\",\"null\"],\"items\":{\"type\":[\"string\",\"null\"]}},\"initialVariations\":{\"type\":[\"array\",\"null\"],\"items\":{\"anyOf\":[{\"$ref\":\"#/$defs/PublicCreateVariationDto\"},{\"type\":\"null\"}]}},\"clientSideVisible\":{\"type\":[\"boolean\",\"null\"]},\"expiresAtUtc\":{\"type\":[\"string\",\"null\"]},\"ownerEmail\":{\"type\":[\"string\",\"null\"]},\"isPermanent\":{\"type\":[\"boolean\",\"null\"]}},\"description\":\"Request body for `POST /api/v1/orgs/{org}/projects/{project}/flags`. Named distinctly\\nfrom the dashboard's own `CreateFeatureFlagRequest`\\n— see PublicCreateEnvironmentRequest for the\\nschemaId-collision rationale (both controllers share one Swagger document). Type\\nis passed through verbatim to Type, which itself takes a\\nbare string (Boolean/String/Number/Json) rather than the domain enum — invalid values surface\\nas the handler's own validation failure, not a controller-level 400. ExpiresAtUtc is\\noptional and advisory; a bare date (`2026-12-01`) means the end of that day in UTC, and a full\\ntimestamp is stored exactly. It must be in the future and requires the flag-expiration feature\\n(400 `EXPIRY_IN_PAST` / `FEATURE_NOT_ENABLED`). OwnerEmail optionally names\\nthe owner (an active member, matched case-insensitively; requires the flag-ownership feature —\\n400 `OWNER_NOT_MEMBER` / `FEATURE_NOT_ENABLED`). Omitted, the owner is the caller when\\nthe caller is a member, so a service token creates an unowned flag. IsPermanent marks\\nthe flag permanent (a kill switch, ops toggle or entitlement that is meant to stay; never reported for\\ncleanup). It requires the flag-permanence feature and can't be combined with ExpiresAtUtc\\n(400 `FEATURE_NOT_ENABLED` / `PERMANENT_FLAG_HAS_EXPIRY`).\"},\"PublicCreateProjectRequest\":{\"type\":[\"object\",\"null\"],\"properties\":{\"key\":{\"type\":[\"string\",\"null\"]},\"name\":{\"type\":[\"string\",\"null\"]},\"description\":{\"type\":[\"string\",\"null\"]}},\"description\":\"Request body for `POST /api/v1/orgs/{org}/projects`. Named distinctly from the\\ndashboard's own `CreateProjectRequest` — both\\ncontrollers share one Swagger document (see `Program.cs`'s `AddSwaggerGen``DocInclusionPredicate` override), and Swashbuckle's default schemaId is the bare type\\nname, so an identical name in either namespace throws a schemaId collision at\\n`--export-openapi` time.\"},\"PublicCreateSdkKeyRequest\":{\"type\":[\"object\",\"null\"],\"properties\":{\"name\":{\"type\":[\"string\",\"null\"]},\"description\":{\"type\":[\"string\",\"null\"]},\"type\":{\"type\":[\"string\",\"null\"],\"description\":\"Allowed values: ClientSide, ServerSide.\"}},\"description\":\"Request body for `POST /api/v1/orgs/{org}/projects/{project}/environments/{env}/sdk-keys`.\\nType is a bare string (\\\"ClientSide\\\" or \\\"ServerSide\\\") — the controller parses it\\nagainst a NAME allowlist (not a bare TryParse``1(ReadOnlySpan{Char},Boolean,``0@), which would\\nsilently accept a numeric string like `\\\"0\\\"`) before dispatching. Named distinctly from\\nthe dashboard's own `CreateSdkKeyRequest` — both\\ncontrollers share one Swagger document, and an identical type name in either namespace throws\\na schemaId collision at `--export-openapi` time.\"},\"PublicCreateSdkKeyResponse\":{\"type\":[\"object\",\"null\"],\"properties\":{\"id\":{\"type\":[\"string\",\"null\"]},\"key\":{\"type\":[\"string\",\"null\"]},\"lastFourCharacters\":{\"type\":[\"string\",\"null\"]},\"type\":{\"type\":[\"string\",\"null\"],\"description\":\"Allowed values: ClientSide, ServerSide.\"},\"name\":{\"type\":[\"string\",\"null\"]},\"createdAt\":{\"type\":[\"string\",\"null\"]},\"warning\":{\"type\":[\"string\",\"null\"]}},\"description\":\"Response body for a successful `POST .../sdk-keys`. The ONLY place the plaintext\\nKey is ever returned — PublicSdkKeyResponse (list/get) carries just\\n`LastFourCharacters`. Built directly from CreateSdkKeyResult rather than a\\nre-fetch (unlike `FeatureFlagsController.Create`'s re-fetch pattern) because the\\nplaintext key only ever exists on the create result — a subsequent\\nGetSdkKeyQuery can't recover it.\"},\"PublicCreateSegmentRequest\":{\"type\":[\"object\",\"null\"],\"properties\":{\"key\":{\"type\":[\"string\",\"null\"]},\"name\":{\"type\":[\"string\",\"null\"]},\"description\":{\"type\":[\"string\",\"null\"]},\"conditions\":{\"type\":[\"array\",\"null\"],\"items\":{\"anyOf\":[{\"$ref\":\"#/$defs/PublicSegmentConditionDto\"},{\"type\":\"null\"}]}}},\"description\":\"Request body for `POST /api/v1/orgs/{org}/projects/{project}/segments`. Named distinctly\\nfrom the dashboard's own `CreateUserSegmentRequest` — see\\nPublicCreateEnvironmentRequest for the schemaId-collision rationale (both\\ncontrollers share one Swagger document).\"},\"PublicCreateTargetingRuleRequest\":{\"type\":[\"object\",\"null\"],\"properties\":{\"description\":{\"type\":[\"string\",\"null\"]},\"variationId\":{\"type\":[\"string\",\"null\"]},\"rolloutPercentage\":{\"type\":[\"integer\",\"null\"]},\"userSegmentId\":{\"type\":[\"string\",\"null\"]},\"conditionGroups\":{\"type\":[\"array\",\"null\"],\"items\":{\"anyOf\":[{\"$ref\":\"#/$defs/PublicConditionGroupRequest\"},{\"type\":\"null\"}]}}},\"description\":\"Request body for `POST .../rules`. The Application-side\\n`CreateTargetingRuleDto`/`CreateConditionGroupDto`/`CreateConditionDto` all take\\ntheir `Operator` as a bare string, re-parsed CASE-SENSITIVELY inside\\n`CreateTargetingRuleCommandHandler` — `FlagTargetingController`'s mapping helpers\\nvalidate every operator against the enum's member-name allowlist AND canonicalize the casing\\n(e.g. `\\\"in\\\"` → `\\\"In\\\"`) before building the command DTO, so a valid\\ncase-insensitive match never trips the handler's strict re-parse.\"},\"PublicCreateVariationDto\":{\"type\":[\"object\",\"null\"],\"properties\":{\"key\":{\"type\":[\"string\",\"null\"]},\"name\":{\"type\":[\"string\",\"null\"]},\"value\":{\"type\":[\"string\",\"null\"]},\"description\":{\"type\":[\"string\",\"null\"]}},\"description\":\"A variation supplied at flag-creation time. Maps 1:1 onto\\nCreateVariationDto.\"},\"PublicCreateWebhookSubscriptionRequest\":{\"type\":[\"object\",\"null\"],\"properties\":{\"name\":{\"type\":[\"string\",\"null\"]},\"url\":{\"type\":[\"string\",\"null\"]},\"provider\":{\"type\":[\"string\",\"null\"],\"description\":\"Allowed values: GenericHttp.\"},\"eventTypes\":{\"type\":[\"array\",\"null\"],\"items\":{\"type\":[\"string\",\"null\"]}},\"projectIds\":{\"type\":[\"array\",\"null\"],\"items\":{\"type\":[\"string\",\"null\"]}},\"environmentIds\":{\"type\":[\"array\",\"null\"],\"items\":{\"type\":[\"string\",\"null\"]}}},\"description\":\"Creates a webhook subscription. `provider` is `GenericHttp`. Omit `eventTypes`,\\n`projectIds` or `environmentIds` (or send an empty list) to receive every event,\\nproject or environment. A token restricted to specific projects must list at least one project,\\nall of them within its allowlist.\"},\"PublicCreateWebhookSubscriptionResponse\":{\"type\":[\"object\",\"null\"],\"properties\":{\"id\":{\"type\":[\"string\",\"null\"]},\"secret\":{\"type\":[\"string\",\"null\"]},\"warning\":{\"type\":[\"string\",\"null\"]}},\"description\":\"The created subscription's id and its signing secret. The secret is shown only here.\"},\"PublicDefaultFlagLifetimeImpact\":{\"type\":[\"object\",\"null\"],\"properties\":{\"newlyPastExpiry\":{\"type\":[\"integer\",\"null\"]},\"noLongerPastExpiry\":{\"type\":[\"integer\",\"null\"]}},\"description\":\"Response of `GET .../default-flag-lifetime/impact`: how many temporary flags with no expiry\\ndate of their own would newly be past expiry, and how many would stop being past it.\"},\"PublicEnvironmentResponse\":{\"type\":[\"object\",\"null\"],\"properties\":{\"id\":{\"type\":[\"string\",\"null\"]},\"key\":{\"type\":[\"string\",\"null\"]},\"name\":{\"type\":[\"string\",\"null\"]},\"color\":{\"type\":[\"string\",\"null\"]},\"sortOrder\":{\"type\":[\"integer\",\"null\"]},\"_actions\":{\"type\":[\"object\",\"null\"],\"additionalProperties\":{\"anyOf\":[{\"$ref\":\"#/$defs/ActionCapability\"},{\"type\":\"null\"}]},\"description\":\"Capability hints: which operations the authenticated caller may perform on this resource, each with allowed + optional reason. Computed from the caller's role and the resource's state. Injected at runtime; safe to ignore. Present only on top-level resource responses — nested occurrences (e.g. a rule inside a targeting-config response) do not carry it.\"}},\"description\":\"Public representation of an environment, returned by\\n`GET /api/v1/orgs/{org}/projects/{project}/environments` (list),\\n`GET .../environments/{env}` (detail), and the body of a successful create/update. Drops\\ninternal fields (e.g. `ProjectId` is implicit in the route) — mirrors\\n`ProjectResponse`'s minimal shape. Named `PublicEnvironmentResponse` rather than\\n`EnvironmentResponse` (unlike the sibling `ProjectResponse`, which didn't collide)\\nbecause the dashboard already has its own\\n`EnvironmentResponse` — see\\nPublicCreateEnvironmentRequest for the schemaId-collision rationale.\"},\"PublicEnvironmentResponsePagedResult\":{\"type\":[\"object\",\"null\"],\"properties\":{\"items\":{\"type\":[\"array\",\"null\"],\"items\":{\"anyOf\":[{\"$ref\":\"#/$defs/PublicEnvironmentResponse\"},{\"type\":\"null\"}]}},\"next_cursor\":{\"type\":[\"string\",\"null\"]},\"_actions\":{\"type\":[\"object\",\"null\"],\"additionalProperties\":{\"anyOf\":[{\"$ref\":\"#/$defs/ActionCapability\"},{\"type\":\"null\"}]},\"description\":\"Capability hints: which operations the authenticated caller may perform on this resource, each with allowed + optional reason. Computed from the caller's role and the resource's state. Injected at runtime; safe to ignore. Present only on top-level resource responses — nested occurrences (e.g. a rule inside a targeting-config response) do not carry it.\"}},\"description\":\"A page of results plus an opaque cursor for the next page, or null when this is the last\\npage. Wire keys are frozen: `items` (already lowercase under the camelCase policy) and\\n`next_cursor` (pinned explicitly — the policy alone would emit `nextCursor`).\"},\"PublicFlagEnvConfigResponse\":{\"type\":[\"object\",\"null\"],\"properties\":{\"environmentId\":{\"type\":[\"string\",\"null\"]},\"environmentKey\":{\"type\":[\"string\",\"null\"]},\"isEnabled\":{\"type\":[\"boolean\",\"null\"]},\"defaultVariationId\":{\"type\":[\"string\",\"null\"]},\"strategy\":{\"type\":[\"string\",\"null\"],\"description\":\"Allowed values: SingleVariation, PercentageRollout, TargetedRollout.\"},\"prerequisites\":{\"type\":[\"array\",\"null\"],\"items\":{\"anyOf\":[{\"$ref\":\"#/$defs/PublicPrerequisiteDto\"},{\"type\":\"null\"}]}},\"_actions\":{\"type\":[\"object\",\"null\"],\"additionalProperties\":{\"anyOf\":[{\"$ref\":\"#/$defs/ActionCapability\"},{\"type\":\"null\"}]},\"description\":\"Capability hints: which operations the authenticated caller may perform on this resource, each with allowed + optional reason. Computed from the caller's role and the resource's state. Injected at runtime; safe to ignore. Present only on top-level resource responses — nested occurrences (e.g. a rule inside a targeting-config response) do not carry it.\"}},\"description\":\"Public representation of a flag's config within a single environment, returned by\\n`GET .../flags/{flag}/environments` and `GET .../flags/{flag}/environments/{env}`.\\nDrops internal/dashboard-only fields present on FlagEnvironmentConfigurationDto\\n— Id (an internal row id, not part of the public\\ncontract; EnvironmentId is the addressable key), `FeatureFlagId` (implicit\\nin the route), the environment's name/color/sort-order (dashboard display fields — the public\\ncontract only needs the addressable EnvironmentKey), and `Staleness` (an\\ninternal analytics signal).\"},\"PublicFlagListItem\":{\"type\":[\"object\",\"null\"],\"properties\":{\"id\":{\"type\":[\"string\",\"null\"]},\"key\":{\"type\":[\"string\",\"null\"]},\"name\":{\"type\":[\"string\",\"null\"]},\"type\":{\"type\":[\"string\",\"null\"],\"description\":\"Allowed values: Boolean, String, Number, Json.\"},\"isArchived\":{\"type\":[\"boolean\",\"null\"]},\"clientSideVisible\":{\"type\":[\"boolean\",\"null\"]},\"tags\":{\"type\":[\"array\",\"null\"],\"items\":{\"type\":[\"string\",\"null\"]}},\"createdAt\":{\"type\":[\"string\",\"null\"]},\"updatedAt\":{\"type\":[\"string\",\"null\"]},\"expiresAtUtc\":{\"type\":[\"string\",\"null\"]},\"owner\":{\"anyOf\":[{\"$ref\":\"#/$defs/PublicFlagOwner\"},{\"type\":\"null\"}]},\"removalPrUrl\":{\"type\":[\"string\",\"null\"]},\"isPermanent\":{\"type\":[\"boolean\",\"null\"]},\"effectiveExpiresAtUtc\":{\"type\":[\"string\",\"null\"]},\"expirySource\":{\"type\":[\"string\",\"null\"]}},\"description\":\"Public list-item representation of a feature flag, returned by\\n`GET .../flags`. Narrower than PublicFlagResponse — drops\\nVariations (a summary-only shape upstream) and\\nStaleness, matching the \\\"list is narrower than detail\\\"\\nsplit already present on the Application-layer DTOs.\\nEffectiveExpiresAtUtc is when the flag counts as past expiry, from its own ExpiresAtUtc\\nor its project's default lifetime; ExpirySource says which (`explicit` / `project-default`),\\nboth null when neither applies.\"},\"PublicFlagListItemPagedResult\":{\"type\":[\"object\",\"null\"],\"properties\":{\"items\":{\"type\":[\"array\",\"null\"],\"items\":{\"anyOf\":[{\"$ref\":\"#/$defs/PublicFlagListItem\"},{\"type\":\"null\"}]}},\"next_cursor\":{\"type\":[\"string\",\"null\"]},\"_actions\":{\"type\":[\"object\",\"null\"],\"additionalProperties\":{\"anyOf\":[{\"$ref\":\"#/$defs/ActionCapability\"},{\"type\":\"null\"}]},\"description\":\"Capability hints: which operations the authenticated caller may perform on this resource, each with allowed + optional reason. Computed from the caller's role and the resource's state. Injected at runtime; safe to ignore. Present only on top-level resource responses — nested occurrences (e.g. a rule inside a targeting-config response) do not carry it.\"}},\"description\":\"A page of results plus an opaque cursor for the next page, or null when this is the last\\npage. Wire keys are frozen: `items` (already lowercase under the camelCase policy) and\\n`next_cursor` (pinned explicitly — the policy alone would emit `nextCursor`).\"},\"PublicFlagOwner\":{\"type\":[\"object\",\"null\"],\"properties\":{\"id\":{\"type\":[\"string\",\"null\"]},\"email\":{\"type\":[\"string\",\"null\"]},\"name\":{\"type\":[\"string\",\"null\"]}},\"description\":\"A flag's owner as returned on flag reads. Null means the flag is unowned.\"},\"PublicFlagRemovalCandidate\":{\"type\":[\"object\",\"null\"],\"properties\":{\"key\":{\"type\":[\"string\",\"null\"]},\"reason\":{\"type\":[\"string\",\"null\"]},\"treatment\":{\"type\":[\"boolean\",\"null\"]},\"status\":{\"type\":[\"string\",\"null\"]},\"blockedBy\":{\"type\":[\"array\",\"null\"],\"items\":{\"type\":[\"string\",\"null\"]}}},\"description\":\"One dead flag the Featureflip Flag Cleanup Action should remove, returned by\\n`GET .../flags/removal-candidates`. `Treatment` is the kill decision: true → keep the\\nflag's on-branch, false → keep the off-branch. `Status` is the flag's staleness tier\\n(`\\\"Stale\\\"` or `\\\"Dead\\\"`, never `\\\"Active\\\"`) — lets a caller distinguish the two when\\n`?staleness=stale` widens the response to include both tiers. This is the ONLY public\\nprojection of staleness — PublicFlagResponse and PublicFlagListItem\\nstill omit it by design. `BlockedBy` names the live flags that still list this one as a\\nprerequisite, sorted by key; while it is non-empty the flag cannot be archived\\n(`FLAG_HAS_DEPENDENTS`), so a caller should remove those dependents first.\"},\"PublicFlagRemovalCandidatePagedResult\":{\"type\":[\"object\",\"null\"],\"properties\":{\"items\":{\"type\":[\"array\",\"null\"],\"items\":{\"anyOf\":[{\"$ref\":\"#/$defs/PublicFlagRemovalCandidate\"},{\"type\":\"null\"}]}},\"next_cursor\":{\"type\":[\"string\",\"null\"]}},\"description\":\"A page of results plus an opaque cursor for the next page, or null when this is the last\\npage. Wire keys are frozen: `items` (already lowercase under the camelCase policy) and\\n`next_cursor` (pinned explicitly — the policy alone would emit `nextCursor`).\"},\"PublicFlagResponse\":{\"type\":[\"object\",\"null\"],\"properties\":{\"id\":{\"type\":[\"string\",\"null\"]},\"key\":{\"type\":[\"string\",\"null\"]},\"name\":{\"type\":[\"string\",\"null\"]},\"description\":{\"type\":[\"string\",\"null\"]},\"type\":{\"type\":[\"string\",\"null\"],\"description\":\"Allowed values: Boolean, String, Number, Json.\"},\"isArchived\":{\"type\":[\"boolean\",\"null\"]},\"clientSideVisible\":{\"type\":[\"boolean\",\"null\"]},\"tags\":{\"type\":[\"array\",\"null\"],\"items\":{\"type\":[\"string\",\"null\"]}},\"variations\":{\"type\":[\"array\",\"null\"],\"items\":{\"anyOf\":[{\"$ref\":\"#/$defs/PublicVariationResponse\"},{\"type\":\"null\"}]}},\"createdAt\":{\"type\":[\"string\",\"null\"]},\"updatedAt\":{\"type\":[\"string\",\"null\"]},\"expiresAtUtc\":{\"type\":[\"string\",\"null\"]},\"owner\":{\"anyOf\":[{\"$ref\":\"#/$defs/PublicFlagOwner\"},{\"type\":\"null\"}]},\"removalPrUrl\":{\"type\":[\"string\",\"null\"]},\"isPermanent\":{\"type\":[\"boolean\",\"null\"]},\"effectiveExpiresAtUtc\":{\"type\":[\"string\",\"null\"]},\"expirySource\":{\"type\":[\"string\",\"null\"]},\"_actions\":{\"type\":[\"object\",\"null\"],\"additionalProperties\":{\"anyOf\":[{\"$ref\":\"#/$defs/ActionCapability\"},{\"type\":\"null\"}]},\"description\":\"Capability hints: which operations the authenticated caller may perform on this resource, each with allowed + optional reason. Computed from the caller's role and the resource's state. Injected at runtime; safe to ignore. Present only on top-level resource responses — nested occurrences (e.g. a rule inside a targeting-config response) do not carry it.\"}},\"description\":\"Public detail representation of a feature flag, returned by\\n`GET .../flags/{flag}` and the body of a successful create. Drops internal/dashboard-only\\nfields present on FeatureFlagDto — notably `ProjectId` (implicit in the\\nroute) and `Staleness` (an internal analytics signal, not part of the public contract).\\nEffectiveExpiresAtUtc is when the flag counts as past expiry, from its own ExpiresAtUtc\\nor its project's default lifetime; ExpirySource says which (`explicit` / `project-default`),\\nboth null when neither applies.\"},\"PublicPrerequisiteDto\":{\"type\":[\"object\",\"null\"],\"properties\":{\"prerequisiteFlagKey\":{\"type\":[\"string\",\"null\"]},\"expectedVariationKey\":{\"type\":[\"string\",\"null\"]}},\"description\":\"A single prerequisite entry on a flag's per-environment config. Maps 1:1 onto\\nPrerequisiteDto (read side) and PrerequisiteInput (write side).\"},\"PublicPutTargetingRequest\":{\"type\":[\"object\",\"null\"],\"properties\":{\"rules\":{\"type\":[\"array\",\"null\"],\"items\":{\"anyOf\":[{\"$ref\":\"#/$defs/PublicCreateTargetingRuleRequest\"},{\"type\":\"null\"}]}}},\"description\":\"Request body for `PUT .../targeting` — a declarative full replace of the environment's\\nordered targeting rule set. Rules are applied in array order (index = priority, so the array\\norder IS the evaluation order). Rules is nullable so that an OMITTED `rules`\\nfield (a body of `{}`) binds successfully instead of failing ASP.NET Core's implicit\\nrequired-property check on non-nullable reference types — both an omitted `rules` field\\nand an explicit empty list (`{ \\\"rules\\\": [] }`) clear all targeting rules for the\\nenvironment. Each element is a PublicCreateTargetingRuleRequest (identical to the\\n`POST .../rules` body), so `UserSegmentId` is supported — segment-scoped rules can be\\nsubmitted through this endpoint. The replace is atomic (all-or-nothing): the whole rule set is\\nswapped in one transaction, or nothing changes.\"},\"PublicReorderRulesRequest\":{\"type\":[\"object\",\"null\"],\"properties\":{\"ruleIds\":{\"type\":[\"array\",\"null\"],\"items\":{\"type\":[\"string\",\"null\"]}}},\"description\":\"Request body for `PUT .../rules/reorder`. Must include every existing rule id exactly\\nonce — `ReorderTargetingRulesCommandHandler` rejects duplicate, omitted, or unknown rule\\nids with `BadRequestException`, surfaced as an HTTP 400 `bad_request` envelope\\ncarrying the handler's message.\"},\"PublicRotateWebhookSecretResponse\":{\"type\":[\"object\",\"null\"],\"properties\":{\"secretId\":{\"type\":[\"string\",\"null\"]},\"secret\":{\"type\":[\"string\",\"null\"]},\"warning\":{\"type\":[\"string\",\"null\"]}},\"description\":\"A newly added signing secret. The secret is shown only here.\"},\"PublicSdkKeyResponse\":{\"type\":[\"object\",\"null\"],\"properties\":{\"id\":{\"type\":[\"string\",\"null\"]},\"name\":{\"type\":[\"string\",\"null\"]},\"description\":{\"type\":[\"string\",\"null\"]},\"type\":{\"type\":[\"string\",\"null\"],\"description\":\"Allowed values: ClientSide, ServerSide.\"},\"lastFourCharacters\":{\"type\":[\"string\",\"null\"]},\"isActive\":{\"type\":[\"boolean\",\"null\"]},\"createdAt\":{\"type\":[\"string\",\"null\"]},\"lastUsedAt\":{\"type\":[\"string\",\"null\"]},\"revokedAt\":{\"type\":[\"string\",\"null\"]},\"_actions\":{\"type\":[\"object\",\"null\"],\"additionalProperties\":{\"anyOf\":[{\"$ref\":\"#/$defs/ActionCapability\"},{\"type\":\"null\"}]},\"description\":\"Capability hints: which operations the authenticated caller may perform on this resource, each with allowed + optional reason. Computed from the caller's role and the resource's state. Injected at runtime; safe to ignore. Present only on top-level resource responses — nested occurrences (e.g. a rule inside a targeting-config response) do not carry it.\"}},\"description\":\"Public representation of an SDK key, returned by\\n`GET /api/v1/orgs/{org}/projects/{project}/environments/{env}/sdk-keys` (list) and\\n`GET .../sdk-keys/{sdkKey}` (detail). Deliberately narrower than\\nSdkKeyDto — drops `RevokedBy` (an internal user id) and\\n`EnvironmentId` (implicit in the route), and carries no plaintext key field: only\\nLastFourCharacters is ever exposed once the key has been created. The plaintext\\nkey is a one-time value that appears solely in PublicCreateSdkKeyResponse.\"},\"PublicSdkKeyResponsePagedResult\":{\"type\":[\"object\",\"null\"],\"properties\":{\"items\":{\"type\":[\"array\",\"null\"],\"items\":{\"anyOf\":[{\"$ref\":\"#/$defs/PublicSdkKeyResponse\"},{\"type\":\"null\"}]}},\"next_cursor\":{\"type\":[\"string\",\"null\"]},\"_actions\":{\"type\":[\"object\",\"null\"],\"additionalProperties\":{\"anyOf\":[{\"$ref\":\"#/$defs/ActionCapability\"},{\"type\":\"null\"}]},\"description\":\"Capability hints: which operations the authenticated caller may perform on this resource, each with allowed + optional reason. Computed from the caller's role and the resource's state. Injected at runtime; safe to ignore. Present only on top-level resource responses — nested occurrences (e.g. a rule inside a targeting-config response) do not carry it.\"}},\"description\":\"A page of results plus an opaque cursor for the next page, or null when this is the last\\npage. Wire keys are frozen: `items` (already lowercase under the camelCase policy) and\\n`next_cursor` (pinned explicitly — the policy alone would emit `nextCursor`).\"},\"PublicSegmentConditionDto\":{\"type\":[\"object\",\"null\"],\"properties\":{\"attribute\":{\"type\":[\"string\",\"null\"]},\"operator\":{\"type\":[\"string\",\"null\"],\"description\":\"Allowed values: Equals, NotEquals, Contains, NotContains, GreaterThan, LessThan, GreaterThanOrEqual, LessThanOrEqual, In, NotIn, MatchesRegex, StartsWith, EndsWith, Before, After, SemverEquals, SemverGreaterThan, SemverGreaterThanOrEqual, SemverLessThan, SemverLessThanOrEqual.\"},\"values\":{\"type\":[\"array\",\"null\"],\"items\":{\"type\":[\"string\",\"null\"]}},\"negate\":{\"type\":[\"boolean\",\"null\"]}},\"description\":\"A single targeting condition on a segment, used both as input (create/update) and output\\n(Conditions). Operator is a bare string on\\nboth sides of the wire — on write it's parsed to\\nOperatorType by `SegmentsController` (an unknown\\nvalue fails closed with a 400, never silently dropped or defaulted); on read it's mapped\\nstraight through from Operator, which is already a string.\"},\"PublicSegmentResponse\":{\"type\":[\"object\",\"null\"],\"properties\":{\"id\":{\"type\":[\"string\",\"null\"]},\"key\":{\"type\":[\"string\",\"null\"]},\"name\":{\"type\":[\"string\",\"null\"]},\"description\":{\"type\":[\"string\",\"null\"]},\"conditions\":{\"type\":[\"array\",\"null\"],\"items\":{\"anyOf\":[{\"$ref\":\"#/$defs/PublicSegmentConditionDto\"},{\"type\":\"null\"}]}},\"createdAt\":{\"type\":[\"string\",\"null\"]},\"updatedAt\":{\"type\":[\"string\",\"null\"]},\"_actions\":{\"type\":[\"object\",\"null\"],\"additionalProperties\":{\"anyOf\":[{\"$ref\":\"#/$defs/ActionCapability\"},{\"type\":\"null\"}]},\"description\":\"Capability hints: which operations the authenticated caller may perform on this resource, each with allowed + optional reason. Computed from the caller's role and the resource's state. Injected at runtime; safe to ignore. Present only on top-level resource responses — nested occurrences (e.g. a rule inside a targeting-config response) do not carry it.\"}},\"description\":\"Public representation of a user segment, returned by `GET .../segments`,\\n`GET .../segments/{segment}`, and the body of a successful create.\"},\"PublicSegmentResponsePagedResult\":{\"type\":[\"object\",\"null\"],\"properties\":{\"items\":{\"type\":[\"array\",\"null\"],\"items\":{\"anyOf\":[{\"$ref\":\"#/$defs/PublicSegmentResponse\"},{\"type\":\"null\"}]}},\"next_cursor\":{\"type\":[\"string\",\"null\"]},\"_actions\":{\"type\":[\"object\",\"null\"],\"additionalProperties\":{\"anyOf\":[{\"$ref\":\"#/$defs/ActionCapability\"},{\"type\":\"null\"}]},\"description\":\"Capability hints: which operations the authenticated caller may perform on this resource, each with allowed + optional reason. Computed from the caller's role and the resource's state. Injected at runtime; safe to ignore. Present only on top-level resource responses — nested occurrences (e.g. a rule inside a targeting-config response) do not carry it.\"}},\"description\":\"A page of results plus an opaque cursor for the next page, or null when this is the last\\npage. Wire keys are frozen: `items` (already lowercase under the camelCase policy) and\\n`next_cursor` (pinned explicitly — the policy alone would emit `nextCursor`).\"},\"PublicSetDefaultFlagLifetimeRequest\":{\"type\":[\"object\",\"null\"],\"properties\":{\"days\":{\"type\":[\"integer\",\"null\"]}},\"description\":\"Request body for `PUT /api/v1/orgs/{org}/projects/{project}/default-flag-lifetime`. `days`\\nis 1–3650; null (or an empty body) removes the default.\"},\"PublicSetFlagExpiryRequest\":{\"type\":[\"object\",\"null\"],\"properties\":{\"expiresAtUtc\":{\"type\":[\"string\",\"null\"]}},\"description\":\"Request body for `PUT /api/v1/orgs/{org}/projects/{project}/flags/{flag}/expiry`. Expiry is a\\ndedicated sub-resource, deliberately NOT a field on PublicUpdateFeatureFlagRequest: that\\nPUT is a full replace, so a new nullable field there would let any client that doesn't know about\\nexpiry (the Terraform provider included) wipe it on every update. A bare date means the end of that\\nUTC day; a full timestamp is stored exactly.\"},\"PublicSetFlagOwnerRequest\":{\"type\":[\"object\",\"null\"],\"properties\":{\"email\":{\"type\":[\"string\",\"null\"]}},\"description\":\"Request body for `PUT /api/v1/orgs/{org}/projects/{project}/flags/{flag}/owner`. A dedicated\\nsub-resource for the same reason as expiry: the main flag PUT is a full replace, so an owner field\\nthere would let every client that doesn't know about owners wipe one. `email` must belong\\nto an active member of the organization (case-insensitive).\"},\"PublicSetFlagRemovalPrRequest\":{\"type\":[\"object\",\"null\"],\"properties\":{\"url\":{\"type\":[\"string\",\"null\"]}},\"description\":\"Request body for `PUT /api/v1/orgs/{org}/projects/{project}/flags/{flag}/removal-pr`: the\\npull request that removes this flag from code, as opened by the Featureflip Flag Cleanup Action.\\nA dedicated sub-resource for the same reason as expiry and owner. `url` must be an https\\npull request URL (`https://{host}/{owner}/{repo}/pull/{number}`).\"},\"PublicTargetingConfigResponse\":{\"type\":[\"object\",\"null\"],\"properties\":{\"isEnabled\":{\"type\":[\"boolean\",\"null\"]},\"rules\":{\"type\":[\"array\",\"null\"],\"items\":{\"anyOf\":[{\"$ref\":\"#/$defs/PublicTargetingRuleResponse\"},{\"type\":\"null\"}]}},\"_actions\":{\"type\":[\"object\",\"null\"],\"additionalProperties\":{\"anyOf\":[{\"$ref\":\"#/$defs/ActionCapability\"},{\"type\":\"null\"}]},\"description\":\"Capability hints: which operations the authenticated caller may perform on this resource, each with allowed + optional reason. Computed from the caller's role and the resource's state. Injected at runtime; safe to ignore. Present only on top-level resource responses — nested occurrences (e.g. a rule inside a targeting-config response) do not carry it.\"}},\"description\":\"The full targeting configuration for a flag in a single environment, returned by\\n`GET .../targeting`. Maps 1:1 onto TargetingConfigurationDto — including\\nits IsEnabled field, which reflects the parent\\n`FlagEnvironmentConfiguration.IsEnabled` (whether the flag is on in this environment at\\nall), not anything specific to targeting itself.\"},\"PublicTargetingRuleResponse\":{\"type\":[\"object\",\"null\"],\"properties\":{\"id\":{\"type\":[\"string\",\"null\"]},\"priority\":{\"type\":[\"integer\",\"null\"]},\"description\":{\"type\":[\"string\",\"null\"]},\"variationId\":{\"type\":[\"string\",\"null\"]},\"rolloutPercentage\":{\"type\":[\"integer\",\"null\"]},\"userSegmentId\":{\"type\":[\"string\",\"null\"]},\"conditionGroups\":{\"type\":[\"array\",\"null\"],\"items\":{\"anyOf\":[{\"$ref\":\"#/$defs/PublicConditionGroupResponse\"},{\"type\":\"null\"}]}},\"_actions\":{\"type\":[\"object\",\"null\"],\"additionalProperties\":{\"anyOf\":[{\"$ref\":\"#/$defs/ActionCapability\"},{\"type\":\"null\"}]},\"description\":\"Capability hints: which operations the authenticated caller may perform on this resource, each with allowed + optional reason. Computed from the caller's role and the resource's state. Injected at runtime; safe to ignore. Present only on top-level resource responses — nested occurrences (e.g. a rule inside a targeting-config response) do not carry it.\"}},\"description\":\"A single targeting rule, returned by `GET .../rules/{ruleId}`,\\n`POST .../rules` (201 body), and nested within PublicTargetingConfigResponse.\\nMaps 1:1 onto TargetingRuleDto, dropping nothing — every field on the\\nApplication DTO is part of the public contract.\"},\"PublicTestWebhookSubscriptionResponse\":{\"type\":[\"object\",\"null\"],\"properties\":{\"webhookEventId\":{\"type\":[\"string\",\"null\"]},\"webhookDeliveryId\":{\"type\":[\"string\",\"null\"]}},\"description\":\"The queued test event and its delivery. Poll the delivery log for the outcome.\"},\"PublicToggleFlagRequest\":{\"type\":[\"object\",\"null\"],\"properties\":{\"enabled\":{\"type\":[\"boolean\",\"null\"]}},\"description\":\"Request body for `POST .../flags/{flag}/environments/{env}/toggle`. Named distinctly from\\nthe dashboard's own `ToggleFlagInEnvironmentRequest`\\n(which uses `IsEnabled`) — the public field is named Enabled per the phase-3c\\ncontract. Enabled is a NULLABLE `bool?` deliberately: a non-nullable value\\ntype would bind an omitted/empty body to `false` with no validation error (ASP.NET's\\nimplicit-required only covers non-nullable reference types, not value types), silently disabling\\nthe flag. The controller rejects a null Enabled with a `validation_failed` 400\\nso the intent is never ambiguous (#1806).\"},\"PublicUpdateEnvironmentRequest\":{\"type\":[\"object\",\"null\"],\"properties\":{\"name\":{\"type\":[\"string\",\"null\"]},\"color\":{\"type\":[\"string\",\"null\"]},\"sortOrder\":{\"type\":[\"integer\",\"null\"]}},\"description\":\"Request body for `PUT /api/v1/orgs/{org}/projects/{project}/environments/{env}`. See\\nPublicCreateEnvironmentRequest for why this is named distinctly from the\\ndashboard's `UpdateEnvironmentRequest`.\"},\"PublicUpdateFeatureFlagRequest\":{\"type\":[\"object\",\"null\"],\"properties\":{\"name\":{\"type\":[\"string\",\"null\"]},\"description\":{\"type\":[\"string\",\"null\"]},\"tags\":{\"type\":[\"array\",\"null\"],\"items\":{\"type\":[\"string\",\"null\"]}},\"clientSideVisible\":{\"type\":[\"boolean\",\"null\"]}},\"description\":\"Request body for `PUT /api/v1/orgs/{org}/projects/{project}/flags/{flag}`. See\\nPublicCreateFeatureFlagRequest for why this is named distinctly from the\\ndashboard's `UpdateFeatureFlagRequest`. The flag's key and type are immutable via this\\nendpoint (mirrors `UpdateFeatureFlagCommand`, which has no `Key`/`Type`).\"},\"PublicUpdateFlagEnvConfigRequest\":{\"type\":[\"object\",\"null\"],\"properties\":{\"defaultVariationId\":{\"type\":[\"string\",\"null\"]},\"strategy\":{\"type\":[\"string\",\"null\"],\"description\":\"Allowed values: SingleVariation, PercentageRollout, TargetedRollout.\"},\"prerequisites\":{\"type\":[\"array\",\"null\"],\"items\":{\"anyOf\":[{\"$ref\":\"#/$defs/PublicPrerequisiteDto\"},{\"type\":\"null\"}]}}},\"description\":\"Request body for `PUT .../flags/{flag}/environments/{env}`. Named distinctly from the\\ndashboard's own `UpdateFlagEnvironmentConfigurationRequest`\\n— see PublicCreateEnvironmentRequest for the schemaId-collision rationale (both\\ncontrollers share one Swagger document). Strategy is passed through verbatim to\\nStrategy\\n— the existing command validator name-allowlists it (SingleVariation/PercentageRollout/\\nTargetedRollout or null), so an invalid value surfaces as the handler's own 400, not a\\ncontroller-level guard. A null Prerequisites leaves prerequisites untouched; a\\nnon-null list replaces them wholesale (mirrors UpdateFlagPrerequisitesCommand's\\nreplace-all semantics).\"},\"PublicUpdateProjectRequest\":{\"type\":[\"object\",\"null\"],\"properties\":{\"name\":{\"type\":[\"string\",\"null\"]},\"description\":{\"type\":[\"string\",\"null\"]}},\"description\":\"Request body for `PUT /api/v1/orgs/{org}/projects/{project}`. See\\nPublicCreateProjectRequest for why this is named distinctly from the\\ndashboard's `UpdateProjectRequest`.\"},\"PublicUpdateSdkKeyRequest\":{\"type\":[\"object\",\"null\"],\"properties\":{\"name\":{\"type\":[\"string\",\"null\"]},\"description\":{\"type\":[\"string\",\"null\"]}},\"description\":\"Request body for `PUT /api/v1/orgs/{org}/projects/{project}/environments/{env}/sdk-keys/{sdkKey}`.\\nType and key material are immutable via this endpoint (mirrors\\nUpdateSdkKeyCommand, which\\ncarries no `Type`).\"},\"PublicUpdateSegmentRequest\":{\"type\":[\"object\",\"null\"],\"properties\":{\"name\":{\"type\":[\"string\",\"null\"]},\"description\":{\"type\":[\"string\",\"null\"]},\"conditions\":{\"type\":[\"array\",\"null\"],\"items\":{\"anyOf\":[{\"$ref\":\"#/$defs/PublicSegmentConditionDto\"},{\"type\":\"null\"}]}}},\"description\":\"Request body for `PUT /api/v1/orgs/{org}/projects/{project}/segments/{segment}`. The\\nsegment's key is immutable via this endpoint (mirrors `UpdateUserSegmentCommand`, which\\ncarries no `Key`).\"},\"PublicUpdateTargetingRuleRequest\":{\"type\":[\"object\",\"null\"],\"properties\":{\"description\":{\"type\":[\"string\",\"null\"]},\"variationId\":{\"type\":[\"string\",\"null\"]},\"rolloutPercentage\":{\"type\":[\"integer\",\"null\"]},\"userSegmentId\":{\"type\":[\"string\",\"null\"]},\"conditionGroups\":{\"type\":[\"array\",\"null\"],\"items\":{\"anyOf\":[{\"$ref\":\"#/$defs/PublicConditionGroupRequest\"},{\"type\":\"null\"}]}}},\"description\":\"Request body for `PUT .../rules/{ruleId}`. Same shape as\\nPublicCreateTargetingRuleRequest — see its remarks on operator\\nvalidation/canonicalization, which applies identically here.\"},\"PublicUpdateVariationRequest\":{\"type\":[\"object\",\"null\"],\"properties\":{\"name\":{\"type\":[\"string\",\"null\"]},\"value\":{\"type\":[\"string\",\"null\"]},\"description\":{\"type\":[\"string\",\"null\"]}},\"description\":\"Request body for\\n`PUT /api/v1/orgs/{org}/projects/{project}/flags/{flag}/variations/{variationId}`. The\\nvariation's key is immutable via this endpoint (mirrors\\n`UpdateVariationCommand`, which\\ncarries no `Key`).\"},\"PublicUpdateWebhookSubscriptionRequest\":{\"type\":[\"object\",\"null\"],\"properties\":{\"name\":{\"type\":[\"string\",\"null\"]},\"url\":{\"type\":[\"string\",\"null\"]},\"eventTypes\":{\"type\":[\"array\",\"null\"],\"items\":{\"type\":[\"string\",\"null\"]}},\"projectIds\":{\"type\":[\"array\",\"null\"],\"items\":{\"type\":[\"string\",\"null\"]}},\"environmentIds\":{\"type\":[\"array\",\"null\"],\"items\":{\"type\":[\"string\",\"null\"]}},\"isEnabled\":{\"type\":[\"boolean\",\"null\"]}},\"description\":\"Replaces a subscription's configuration (full replace — omitted lists become empty, meaning\\n\\\"all\\\"). `isEnabled` is required. The provider cannot be changed.\"},\"PublicVariationResponse\":{\"type\":[\"object\",\"null\"],\"properties\":{\"id\":{\"type\":[\"string\",\"null\"]},\"key\":{\"type\":[\"string\",\"null\"]},\"name\":{\"type\":[\"string\",\"null\"]},\"value\":{\"type\":[\"string\",\"null\"]},\"description\":{\"type\":[\"string\",\"null\"]},\"_actions\":{\"type\":[\"object\",\"null\"],\"additionalProperties\":{\"anyOf\":[{\"$ref\":\"#/$defs/ActionCapability\"},{\"type\":\"null\"}]},\"description\":\"Capability hints: which operations the authenticated caller may perform on this resource, each with allowed + optional reason. Computed from the caller's role and the resource's state. Injected at runtime; safe to ignore. Present only on top-level resource responses — nested occurrences (e.g. a rule inside a targeting-config response) do not carry it.\"}},\"description\":\"A single variation on a flag, as returned in Variations.\"},\"PublicWebhookDeliveryResponse\":{\"type\":[\"object\",\"null\"],\"properties\":{\"id\":{\"type\":[\"string\",\"null\"]},\"webhookEventId\":{\"type\":[\"string\",\"null\"]},\"status\":{\"type\":[\"string\",\"null\"],\"description\":\"Allowed values: Pending, Succeeded, DeadLettered.\"},\"attemptCount\":{\"type\":[\"integer\",\"null\"]},\"nextAttemptAt\":{\"type\":[\"string\",\"null\"]},\"lastResponseStatusCode\":{\"type\":[\"integer\",\"null\"]},\"lastError\":{\"type\":[\"string\",\"null\"]},\"completedAt\":{\"type\":[\"string\",\"null\"]},\"createdAt\":{\"type\":[\"string\",\"null\"]}},\"description\":\"One delivery attempt record. `status` is `Pending` (awaiting its first attempt,\\nbacking off, or in flight), `Succeeded` or `DeadLettered`.\"},\"PublicWebhookDeliveryResponsePagedResult\":{\"type\":[\"object\",\"null\"],\"properties\":{\"items\":{\"type\":[\"array\",\"null\"],\"items\":{\"anyOf\":[{\"$ref\":\"#/$defs/PublicWebhookDeliveryResponse\"},{\"type\":\"null\"}]}},\"next_cursor\":{\"type\":[\"string\",\"null\"]}},\"description\":\"A page of results plus an opaque cursor for the next page, or null when this is the last\\npage. Wire keys are frozen: `items` (already lowercase under the camelCase policy) and\\n`next_cursor` (pinned explicitly — the policy alone would emit `nextCursor`).\"},\"PublicWebhookSecretResponse\":{\"type\":[\"object\",\"null\"],\"properties\":{\"id\":{\"type\":[\"string\",\"null\"]},\"createdAt\":{\"type\":[\"string\",\"null\"]},\"retiredAt\":{\"type\":[\"string\",\"null\"]}},\"description\":\"A signing secret's id and lifecycle. A secret with no `retiredAt` is active and signs every delivery.\"},\"PublicWebhookSubscriptionResponse\":{\"type\":[\"object\",\"null\"],\"properties\":{\"id\":{\"type\":[\"string\",\"null\"]},\"name\":{\"type\":[\"string\",\"null\"]},\"url\":{\"type\":[\"string\",\"null\"]},\"provider\":{\"type\":[\"string\",\"null\"],\"description\":\"Allowed values: GenericHttp.\"},\"isEnabled\":{\"type\":[\"boolean\",\"null\"]},\"eventTypes\":{\"type\":[\"array\",\"null\"],\"items\":{\"type\":[\"string\",\"null\"]}},\"projectIds\":{\"type\":[\"array\",\"null\"],\"items\":{\"type\":[\"string\",\"null\"]}},\"environmentIds\":{\"type\":[\"array\",\"null\"],\"items\":{\"type\":[\"string\",\"null\"]}},\"consecutiveFailureCount\":{\"type\":[\"integer\",\"null\"]},\"autoDisabledAt\":{\"type\":[\"string\",\"null\"]},\"createdAt\":{\"type\":[\"string\",\"null\"]},\"updatedAt\":{\"type\":[\"string\",\"null\"]},\"secrets\":{\"type\":[\"array\",\"null\"],\"items\":{\"anyOf\":[{\"$ref\":\"#/$defs/PublicWebhookSecretResponse\"},{\"type\":\"null\"}]}},\"overPlanLimit\":{\"type\":[\"boolean\",\"null\"]},\"_actions\":{\"type\":[\"object\",\"null\"],\"additionalProperties\":{\"anyOf\":[{\"$ref\":\"#/$defs/ActionCapability\"},{\"type\":\"null\"}]},\"description\":\"Capability hints: which operations the authenticated caller may perform on this resource, each with allowed + optional reason. Computed from the caller's role and the resource's state. Injected at runtime; safe to ignore. Present only on top-level resource responses — nested occurrences (e.g. a rule inside a targeting-config response) do not carry it.\"}},\"description\":\"A webhook subscription. Never carries secret material: signing secrets are listed by id and\\nlifecycle only — the value itself is returned once, when it is created or rotated.\\n`overPlanLimit` is true when the organization has more subscriptions than its plan\\nallows, which happens after a downgrade. The oldest subscriptions up to the limit keep\\ndelivering. The rest receive nothing and cannot be re-enabled, tested or redelivered to until\\nthe plan is upgraded or older subscriptions are deleted.\"},\"PublicWebhookSubscriptionResponsePagedResult\":{\"type\":[\"object\",\"null\"],\"properties\":{\"items\":{\"type\":[\"array\",\"null\"],\"items\":{\"anyOf\":[{\"$ref\":\"#/$defs/PublicWebhookSubscriptionResponse\"},{\"type\":\"null\"}]}},\"next_cursor\":{\"type\":[\"string\",\"null\"]},\"_actions\":{\"type\":[\"object\",\"null\"],\"additionalProperties\":{\"anyOf\":[{\"$ref\":\"#/$defs/ActionCapability\"},{\"type\":\"null\"}]},\"description\":\"Capability hints: which operations the authenticated caller may perform on this resource, each with allowed + optional reason. Computed from the caller's role and the resource's state. Injected at runtime; safe to ignore. Present only on top-level resource responses — nested occurrences (e.g. a rule inside a targeting-config response) do not carry it.\"}},\"description\":\"A page of results plus an opaque cursor for the next page, or null when this is the last\\npage. Wire keys are frozen: `items` (already lowercase under the camelCase policy) and\\n`next_cursor` (pinned explicitly — the policy alone would emit `nextCursor`).\"}}")
|
|
201
|
+
}, m = p.$defs, h = p.endpoints, g = (e) => ({ type: [e, "null"] });
|
|
202
|
+
function _(e) {
|
|
203
|
+
if (!m[e]) throw Error(`no API schema named ${e}`);
|
|
204
|
+
return { $ref: `#/$defs/${e}` };
|
|
205
|
+
}
|
|
206
|
+
function v(e) {
|
|
207
|
+
let t = h[e];
|
|
208
|
+
if (!t) throw Error(`the public API spec has no JSON response for ${e}`);
|
|
209
|
+
return _(t);
|
|
210
|
+
}
|
|
211
|
+
function y(e, t = /* @__PURE__ */ new Set()) {
|
|
212
|
+
if (Array.isArray(e)) for (let n of e) y(n, t);
|
|
213
|
+
else if (e && typeof e == "object") for (let [n, r] of Object.entries(e)) if (n === "$ref" && typeof r == "string" && r.startsWith("#/$defs/")) {
|
|
214
|
+
let e = r.slice(8);
|
|
215
|
+
t.has(e) || (t.add(e), y(m[e], t));
|
|
216
|
+
} else y(r, t);
|
|
217
|
+
return t;
|
|
218
|
+
}
|
|
219
|
+
function b(e) {
|
|
220
|
+
let n = [...y(e)].sort(), r = {
|
|
221
|
+
...e,
|
|
222
|
+
type: "object"
|
|
223
|
+
};
|
|
224
|
+
return n.length && (r.$defs = Object.fromEntries(n.map((e) => [e, m[e]]))), t(r);
|
|
225
|
+
}
|
|
226
|
+
function x(e) {
|
|
227
|
+
let t = h[e];
|
|
228
|
+
if (!t) throw Error(`the public API spec has no JSON response for ${e}`);
|
|
229
|
+
return b({
|
|
230
|
+
...m[t],
|
|
231
|
+
type: "object"
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
function ee(e, t) {
|
|
235
|
+
let n = h[e];
|
|
236
|
+
if (!n) throw Error(`the public API spec has no JSON response for ${e}`);
|
|
237
|
+
let r = m[n];
|
|
238
|
+
return b({
|
|
239
|
+
...r,
|
|
240
|
+
properties: {
|
|
241
|
+
...r.properties,
|
|
242
|
+
...t
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
function S(e, t = [], n) {
|
|
247
|
+
return b({
|
|
248
|
+
properties: e,
|
|
249
|
+
...t.length ? { required: t } : {},
|
|
250
|
+
...n ? { description: n } : {}
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
function C(...e) {
|
|
254
|
+
return b({ anyOf: e });
|
|
255
|
+
}
|
|
256
|
+
function w(e, t = []) {
|
|
257
|
+
return {
|
|
258
|
+
type: "object",
|
|
259
|
+
properties: e,
|
|
260
|
+
...t.length ? { required: t } : {}
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
//#endregion
|
|
264
|
+
//#region src/tools/params.ts
|
|
265
|
+
var T = n.string().describe("Project key"), E = n.string().describe("Flag key or id"), D = n.string().describe("Environment key, e.g. production"), O = {
|
|
266
|
+
limit: n.number().int().min(1).max(100).optional().describe("Page size (default 20)"),
|
|
267
|
+
cursor: n.string().optional().describe("next_cursor from a previous response")
|
|
268
|
+
};
|
|
269
|
+
//#endregion
|
|
270
|
+
//#region src/tools/projects.ts
|
|
271
|
+
function k(e, t) {
|
|
272
|
+
e.registerTool("list_projects", {
|
|
273
|
+
title: "List projects",
|
|
274
|
+
description: "List projects in the Featureflip organization. Paginated: pass cursor from next_cursor to continue.",
|
|
275
|
+
inputSchema: n.object({ ...O }),
|
|
276
|
+
outputSchema: x("GET /api/v1/orgs/{org}/projects"),
|
|
277
|
+
annotations: { readOnlyHint: !0 }
|
|
278
|
+
}, async ({ limit: e, cursor: n }) => f(async () => u(await t.api.request("GET", `/api/v1/orgs/${a(t.org)}/projects`, { query: {
|
|
279
|
+
limit: e,
|
|
280
|
+
cursor: n
|
|
281
|
+
} }))));
|
|
282
|
+
}
|
|
283
|
+
//#endregion
|
|
284
|
+
//#region src/tools/environments.ts
|
|
285
|
+
function A(e, t) {
|
|
286
|
+
e.registerTool("list_environments", {
|
|
287
|
+
title: "List environments",
|
|
288
|
+
description: "List the environments of a project (e.g. development, staging, production).",
|
|
289
|
+
inputSchema: n.object({
|
|
290
|
+
project: T,
|
|
291
|
+
...O
|
|
292
|
+
}),
|
|
293
|
+
outputSchema: x("GET /api/v1/orgs/{org}/projects/{project}/environments"),
|
|
294
|
+
annotations: { readOnlyHint: !0 }
|
|
295
|
+
}, async ({ project: e, limit: n, cursor: r }) => f(async () => u(await t.api.request("GET", `/api/v1/orgs/${a(t.org)}/projects/${a(e)}/environments`, { query: {
|
|
296
|
+
limit: n,
|
|
297
|
+
cursor: r
|
|
298
|
+
} }))));
|
|
299
|
+
}
|
|
300
|
+
//#endregion
|
|
301
|
+
//#region src/tools/segments.ts
|
|
302
|
+
function j(e, t) {
|
|
303
|
+
e.registerTool("list_segments", {
|
|
304
|
+
title: "List user segments",
|
|
305
|
+
description: "List reusable user segments in a project. Segments are referenced from targeting rules via userSegmentId.",
|
|
306
|
+
inputSchema: n.object({
|
|
307
|
+
project: n.string().describe("Project key"),
|
|
308
|
+
...O
|
|
309
|
+
}),
|
|
310
|
+
outputSchema: x("GET /api/v1/orgs/{org}/projects/{project}/segments"),
|
|
311
|
+
annotations: { readOnlyHint: !0 }
|
|
312
|
+
}, async ({ project: e, limit: n, cursor: r }) => f(async () => u(await t.api.request("GET", `/api/v1/orgs/${a(t.org)}/projects/${a(e)}/segments`, { query: {
|
|
313
|
+
limit: n,
|
|
314
|
+
cursor: r
|
|
315
|
+
} })))), e.registerTool("get_segment", {
|
|
316
|
+
title: "Get user segment",
|
|
317
|
+
description: "Get one user segment (its conditions, id, and metadata) by key or id.",
|
|
318
|
+
inputSchema: n.object({
|
|
319
|
+
project: n.string().describe("Project key"),
|
|
320
|
+
segment: n.string().describe("Segment key or id")
|
|
321
|
+
}),
|
|
322
|
+
outputSchema: x("GET /api/v1/orgs/{org}/projects/{project}/segments/{segment}"),
|
|
323
|
+
annotations: { readOnlyHint: !0 }
|
|
324
|
+
}, async ({ project: e, segment: n }) => f(async () => u(await t.api.request("GET", `/api/v1/orgs/${a(t.org)}/projects/${a(e)}/segments/${a(n)}`))));
|
|
325
|
+
}
|
|
326
|
+
//#endregion
|
|
327
|
+
//#region src/tools/owner.ts
|
|
328
|
+
var M = n.string().optional().describe("Only flags with this owner: an email, \"me\" for the token's own user, or \"none\" for unowned flags");
|
|
329
|
+
async function N(e, t) {
|
|
330
|
+
let n = t?.trim();
|
|
331
|
+
if (!n) return;
|
|
332
|
+
if (n.toLowerCase() !== "me") return n;
|
|
333
|
+
let r = await e.api.request("GET", "/api/v1/me");
|
|
334
|
+
if (r.type !== "user" || !r.email) throw Error("owner \"me\" needs a personal access token (ffp_...). A service token has no user, so it owns no flags; pass an owner email instead.");
|
|
335
|
+
return r.email;
|
|
336
|
+
}
|
|
337
|
+
//#endregion
|
|
338
|
+
//#region src/tools/flags.ts
|
|
339
|
+
var P = n.enum([
|
|
340
|
+
"Boolean",
|
|
341
|
+
"String",
|
|
342
|
+
"Number",
|
|
343
|
+
"Json"
|
|
344
|
+
]);
|
|
345
|
+
function F(e, t) {
|
|
346
|
+
let r = (e) => `/api/v1/orgs/${a(t.org)}/projects/${a(e)}/flags`;
|
|
347
|
+
e.registerTool("list_flags", {
|
|
348
|
+
title: "List feature flags",
|
|
349
|
+
description: "List feature flags in a project. Filter with search (key/name substring), tag, type, archived, owner. Each flag carries its owner ({id, email, name}, or null when unowned). Paginated via cursor.",
|
|
350
|
+
inputSchema: n.object({
|
|
351
|
+
project: T,
|
|
352
|
+
search: n.string().optional().describe("Substring of the flag key or name"),
|
|
353
|
+
tag: n.string().optional().describe("Only flags carrying this tag"),
|
|
354
|
+
type: P.optional().describe("Only flags of this type"),
|
|
355
|
+
archived: n.boolean().optional().describe("true = only archived, false = only active"),
|
|
356
|
+
owner: M,
|
|
357
|
+
...O
|
|
358
|
+
}),
|
|
359
|
+
outputSchema: x("GET /api/v1/orgs/{org}/projects/{project}/flags"),
|
|
360
|
+
annotations: { readOnlyHint: !0 }
|
|
361
|
+
}, async ({ project: e, owner: n, ...i }) => f(async () => u(await t.api.request("GET", r(e), { query: {
|
|
362
|
+
...i,
|
|
363
|
+
owner: await N(t, n)
|
|
364
|
+
} })))), e.registerTool("get_flag", {
|
|
365
|
+
title: "Get feature flag",
|
|
366
|
+
description: "Get one feature flag with its variations and metadata, including its owner (null when unowned). Address by flag key or id.",
|
|
367
|
+
inputSchema: n.object({
|
|
368
|
+
project: T,
|
|
369
|
+
flag: E
|
|
370
|
+
}),
|
|
371
|
+
outputSchema: x("GET /api/v1/orgs/{org}/projects/{project}/flags/{flag}"),
|
|
372
|
+
annotations: { readOnlyHint: !0 }
|
|
373
|
+
}, async ({ project: e, flag: n }) => f(async () => u(await t.api.request("GET", `${r(e)}/${a(n)}`)))), e.registerTool("create_flag", {
|
|
374
|
+
title: "Create feature flag",
|
|
375
|
+
description: "Create a feature flag. type is one of Boolean|String|Number|Json. Boolean flags get true/false variations automatically; for other types pass initialVariations. The flag is created in every environment of the project (disabled).",
|
|
376
|
+
inputSchema: n.object({
|
|
377
|
+
project: T,
|
|
378
|
+
key: n.string().describe("Unique flag key, e.g. checkout-v2"),
|
|
379
|
+
name: n.string().describe("Display name"),
|
|
380
|
+
type: P.describe("Value type; Boolean gets true/false variations automatically"),
|
|
381
|
+
description: n.string().optional().describe("What the flag controls"),
|
|
382
|
+
tags: n.array(n.string()).optional().describe("Tags for filtering, e.g. team or area"),
|
|
383
|
+
clientSideVisible: n.boolean().optional().describe("Expose to client-side SDKs (browser/mobile)"),
|
|
384
|
+
initialVariations: n.array(n.object({
|
|
385
|
+
key: n.string(),
|
|
386
|
+
name: n.string().optional(),
|
|
387
|
+
value: n.string().describe("Variation value serialized as a string"),
|
|
388
|
+
description: n.string().optional()
|
|
389
|
+
})).optional().describe("Variations for a String, Number or Json flag (Boolean flags get true/false automatically)"),
|
|
390
|
+
expiresAtUtc: n.string().optional().describe("Optional date (YYYY-MM-DD, end of that UTC day) or ISO-8601 timestamp the flag is expected to be removed by (see set_flag_expiry)"),
|
|
391
|
+
ownerEmail: n.string().optional().describe("Optional owner: the email of an active member (Pro plan and above, see set_flag_owner). Omitted, a personal access token makes its own user the owner and a service token leaves the flag unowned"),
|
|
392
|
+
isPermanent: n.boolean().optional().describe("Mark the flag permanent: a kill switch, ops toggle or entitlement gate that is meant to stay. Permanent flags are never listed as removal candidates or by find_stale_flags. Cannot be combined with expiresAtUtc"),
|
|
393
|
+
idempotency_key: n.string().optional().describe("Idempotency-Key header for safe retries")
|
|
394
|
+
}),
|
|
395
|
+
outputSchema: x("POST /api/v1/orgs/{org}/projects/{project}/flags"),
|
|
396
|
+
annotations: { destructiveHint: !1 }
|
|
397
|
+
}, async ({ project: e, idempotency_key: n, ...i }) => f(async () => u(await t.api.request("POST", r(e), {
|
|
398
|
+
body: i,
|
|
399
|
+
idempotencyKey: n
|
|
400
|
+
})))), e.registerTool("update_flag", {
|
|
401
|
+
title: "Update feature flag metadata",
|
|
402
|
+
description: "Update flag name/description/tags/clientSideVisible. Changes only the fields you pass and keeps the rest; pass description: \"\" to clear it. Key and type are immutable. Use toggle_flag / update_targeting for behavior changes.",
|
|
403
|
+
inputSchema: n.object({
|
|
404
|
+
project: T,
|
|
405
|
+
flag: E,
|
|
406
|
+
name: n.string().optional().describe("New display name"),
|
|
407
|
+
description: n.string().optional().describe("New description; \"\" clears it"),
|
|
408
|
+
tags: n.array(n.string()).optional().describe("Replaces the whole tag list"),
|
|
409
|
+
clientSideVisible: n.boolean().optional().describe("Expose to client-side SDKs (browser/mobile); false hides the flag from them entirely")
|
|
410
|
+
}),
|
|
411
|
+
outputSchema: S({
|
|
412
|
+
project: { type: "string" },
|
|
413
|
+
flag: { type: "string" },
|
|
414
|
+
updated: { type: "boolean" }
|
|
415
|
+
}, [
|
|
416
|
+
"project",
|
|
417
|
+
"flag",
|
|
418
|
+
"updated"
|
|
419
|
+
]),
|
|
420
|
+
annotations: { destructiveHint: !0 }
|
|
421
|
+
}, async ({ project: e, flag: n, name: i, description: o, tags: s, clientSideVisible: c }) => f(async () => {
|
|
422
|
+
let l = `${r(e)}/${a(n)}`, d = i === void 0 || o === void 0 ? await t.api.request("GET", l) : void 0, f = {
|
|
423
|
+
name: i ?? d?.name,
|
|
424
|
+
description: o ?? d?.description,
|
|
425
|
+
tags: s,
|
|
426
|
+
clientSideVisible: c
|
|
427
|
+
};
|
|
428
|
+
return await t.api.request("PUT", l, { body: f }), u({
|
|
429
|
+
project: e,
|
|
430
|
+
flag: n,
|
|
431
|
+
updated: !0
|
|
432
|
+
});
|
|
433
|
+
})), e.registerTool("set_flag_expiry", {
|
|
434
|
+
title: "Set or clear flag expiry",
|
|
435
|
+
description: "Set the date a flag is expected to be removed by, or pass expiresAtUtc: null to clear it. Expiry is advisory: evaluation never changes. Once the date passes, find_stale_flags reports the flag as expired. expiresAtUtc takes a date (2026-12-31), which means the end of that day in UTC, the same as picking it in the dashboard, or a full ISO-8601 timestamp, which is stored exactly. Refused with EXPIRY_IN_PAST for a time that has already passed (today's date is allowed), with FEATURE_NOT_ENABLED while flag expiration is not enabled for the organization, and with PERMANENT_FLAG_HAS_EXPIRY on a permanent flag (make it temporary first with set_flag_permanent). Clearing is always allowed.",
|
|
436
|
+
inputSchema: n.object({
|
|
437
|
+
project: T,
|
|
438
|
+
flag: E,
|
|
439
|
+
expiresAtUtc: n.string().nullable().describe("Date (YYYY-MM-DD, end of that UTC day) or ISO-8601 timestamp, or null to clear the expiry")
|
|
440
|
+
}),
|
|
441
|
+
outputSchema: S({
|
|
442
|
+
project: { type: "string" },
|
|
443
|
+
flag: { type: "string" },
|
|
444
|
+
expiresAtUtc: g("string")
|
|
445
|
+
}, [
|
|
446
|
+
"project",
|
|
447
|
+
"flag",
|
|
448
|
+
"expiresAtUtc"
|
|
449
|
+
]),
|
|
450
|
+
annotations: {
|
|
451
|
+
destructiveHint: !1,
|
|
452
|
+
idempotentHint: !0
|
|
453
|
+
}
|
|
454
|
+
}, async ({ project: e, flag: n, expiresAtUtc: i }) => f(async () => {
|
|
455
|
+
let o = `${r(e)}/${a(n)}/expiry`;
|
|
456
|
+
return i === null ? await t.api.request("DELETE", o) : await t.api.request("PUT", o, { body: { expiresAtUtc: i } }), u({
|
|
457
|
+
project: e,
|
|
458
|
+
flag: n,
|
|
459
|
+
expiresAtUtc: i
|
|
460
|
+
});
|
|
461
|
+
})), e.registerTool("set_flag_permanent", {
|
|
462
|
+
title: "Mark a flag permanent or temporary",
|
|
463
|
+
description: "Mark a flag permanent (a kill switch, ops toggle or entitlement gate that is meant to stay), or pass permanent: false to make it temporary again. Permanent flags are never listed as removal candidates or by find_stale_flags; evaluation never changes. Marking is refused with PERMANENT_FLAG_HAS_EXPIRY while the flag has an expiry date (clear it first with set_flag_expiry and expiresAtUtc: null), with FEATURE_NOT_ENABLED while permanent flags are not enabled for the organization, and on an archived flag. Making a flag temporary is always allowed.",
|
|
464
|
+
inputSchema: n.object({
|
|
465
|
+
project: T,
|
|
466
|
+
flag: E,
|
|
467
|
+
permanent: n.boolean().describe("true to mark the flag permanent, false to make it temporary")
|
|
468
|
+
}),
|
|
469
|
+
outputSchema: S({
|
|
470
|
+
project: { type: "string" },
|
|
471
|
+
flag: { type: "string" },
|
|
472
|
+
permanent: { type: "boolean" }
|
|
473
|
+
}, [
|
|
474
|
+
"project",
|
|
475
|
+
"flag",
|
|
476
|
+
"permanent"
|
|
477
|
+
]),
|
|
478
|
+
annotations: {
|
|
479
|
+
destructiveHint: !1,
|
|
480
|
+
idempotentHint: !0
|
|
481
|
+
}
|
|
482
|
+
}, async ({ project: e, flag: n, permanent: i }) => f(async () => {
|
|
483
|
+
let o = `${r(e)}/${a(n)}/permanent`;
|
|
484
|
+
return await t.api.request(i ? "PUT" : "DELETE", o), u({
|
|
485
|
+
project: e,
|
|
486
|
+
flag: n,
|
|
487
|
+
permanent: i
|
|
488
|
+
});
|
|
489
|
+
})), e.registerTool("set_flag_owner", {
|
|
490
|
+
title: "Set or clear flag owner",
|
|
491
|
+
description: "Name the person responsible for a flag, or pass owner: null to leave it unowned. owner is the email of an active member of the organization, or \"me\" for the token's own user (needs a personal access token). The owner gets the flag's cleanup notices; evaluation never changes. Refused with OWNER_NOT_MEMBER for an email that is not an active member, and with PLAN_FEATURE_UNAVAILABLE below the Pro plan. Clearing is always allowed.",
|
|
492
|
+
inputSchema: n.object({
|
|
493
|
+
project: T,
|
|
494
|
+
flag: E,
|
|
495
|
+
owner: n.string().nullable().describe("Owner email, \"me\" for the token's own user, or null (or \"none\") to clear the owner")
|
|
496
|
+
}),
|
|
497
|
+
outputSchema: S({
|
|
498
|
+
project: { type: "string" },
|
|
499
|
+
flag: { type: "string" },
|
|
500
|
+
owner: {
|
|
501
|
+
...g("string"),
|
|
502
|
+
description: "Owner email now set, or null when unowned"
|
|
503
|
+
}
|
|
504
|
+
}, [
|
|
505
|
+
"project",
|
|
506
|
+
"flag",
|
|
507
|
+
"owner"
|
|
508
|
+
]),
|
|
509
|
+
annotations: {
|
|
510
|
+
destructiveHint: !1,
|
|
511
|
+
idempotentHint: !0
|
|
512
|
+
}
|
|
513
|
+
}, async ({ project: e, flag: n, owner: i }) => f(async () => {
|
|
514
|
+
let o = `${r(e)}/${a(n)}/owner`, s = i?.trim().toLowerCase() === "none" ? void 0 : await N(t, i ?? void 0);
|
|
515
|
+
return s === void 0 ? await t.api.request("DELETE", o) : await t.api.request("PUT", o, { body: { email: s } }), u({
|
|
516
|
+
project: e,
|
|
517
|
+
flag: n,
|
|
518
|
+
owner: s ?? null
|
|
519
|
+
});
|
|
520
|
+
})), e.registerTool("delete_flag", {
|
|
521
|
+
title: "Delete feature flag",
|
|
522
|
+
description: "PERMANENTLY delete a flag across all environments. Fails with FLAG_HAS_DEPENDENTS if other flags use it as a prerequisite. Prefer archive_flag unless the flag must be fully removed.",
|
|
523
|
+
inputSchema: n.object({
|
|
524
|
+
project: T,
|
|
525
|
+
flag: E
|
|
526
|
+
}),
|
|
527
|
+
outputSchema: S({ deleted: { type: "string" } }, ["deleted"]),
|
|
528
|
+
annotations: { destructiveHint: !0 }
|
|
529
|
+
}, async ({ project: e, flag: n }) => f(async () => (await t.api.request("DELETE", `${r(e)}/${a(n)}`), u({ deleted: n })))), e.registerTool("archive_flag", {
|
|
530
|
+
title: "Archive feature flag",
|
|
531
|
+
description: "Archive a flag (soft-hide, evaluation stops serving it). Reversible with restore_flag. Archive dependents first: refused with FLAG_HAS_DEPENDENTS while another live flag lists this one as a prerequisite, so archive those flags (blockedBy in find_stale_flags / list_removal_candidates) first. Refused with FLAG_RECENTLY_EVALUATED while live traffic is still evaluating the flag, because archiving makes every caller fall back to its own hardcoded default — normally that means the code removal has merged but not deployed yet, and the refusal clears itself once it has.",
|
|
532
|
+
inputSchema: n.object({
|
|
533
|
+
project: T,
|
|
534
|
+
flag: E,
|
|
535
|
+
force: n.boolean().optional().describe("Archive even though traffic is still evaluating the flag. Only for clients that can never be updated (old mobile app versions), where traffic will never drain on its own.")
|
|
536
|
+
}),
|
|
537
|
+
outputSchema: S({
|
|
538
|
+
flag: { type: "string" },
|
|
539
|
+
archived: { type: "boolean" }
|
|
540
|
+
}, ["flag", "archived"]),
|
|
541
|
+
annotations: { destructiveHint: !0 }
|
|
542
|
+
}, async ({ project: e, flag: n, force: i }) => f(async () => (await t.api.request("POST", `${r(e)}/${a(n)}/archive`, { query: i ? { force: !0 } : {} }), u({
|
|
543
|
+
flag: n,
|
|
544
|
+
archived: !0
|
|
545
|
+
})))), e.registerTool("restore_flag", {
|
|
546
|
+
title: "Restore archived flag",
|
|
547
|
+
description: "Restore a previously archived flag.",
|
|
548
|
+
inputSchema: n.object({
|
|
549
|
+
project: T,
|
|
550
|
+
flag: E
|
|
551
|
+
}),
|
|
552
|
+
outputSchema: S({
|
|
553
|
+
flag: { type: "string" },
|
|
554
|
+
archived: { type: "boolean" }
|
|
555
|
+
}, ["flag", "archived"]),
|
|
556
|
+
annotations: { destructiveHint: !1 }
|
|
557
|
+
}, async ({ project: e, flag: n }) => f(async () => (await t.api.request("POST", `${r(e)}/${a(n)}/restore`), u({
|
|
558
|
+
flag: n,
|
|
559
|
+
archived: !1
|
|
560
|
+
}))));
|
|
561
|
+
}
|
|
562
|
+
//#endregion
|
|
563
|
+
//#region src/tools/flag-environments.ts
|
|
564
|
+
var I = n.enum([
|
|
565
|
+
"SingleVariation",
|
|
566
|
+
"PercentageRollout",
|
|
567
|
+
"TargetedRollout"
|
|
568
|
+
]);
|
|
569
|
+
function te(e, t) {
|
|
570
|
+
let r = (e, n) => `/api/v1/orgs/${a(t.org)}/projects/${a(e)}/flags/${a(n)}`;
|
|
571
|
+
e.registerTool("toggle_flag", {
|
|
572
|
+
title: "Toggle flag in environment",
|
|
573
|
+
description: "Enable or disable a flag in ONE environment. Affects live evaluation immediately — double-check the environment.",
|
|
574
|
+
inputSchema: n.object({
|
|
575
|
+
project: T,
|
|
576
|
+
flag: E,
|
|
577
|
+
environment: D,
|
|
578
|
+
enabled: n.boolean().describe("true to turn the flag on in this environment, false to turn it off")
|
|
579
|
+
}),
|
|
580
|
+
outputSchema: S({
|
|
581
|
+
project: { type: "string" },
|
|
582
|
+
flag: { type: "string" },
|
|
583
|
+
environment: { type: "string" },
|
|
584
|
+
enabled: { type: "boolean" }
|
|
585
|
+
}, [
|
|
586
|
+
"project",
|
|
587
|
+
"flag",
|
|
588
|
+
"environment",
|
|
589
|
+
"enabled"
|
|
590
|
+
]),
|
|
591
|
+
annotations: { destructiveHint: !0 }
|
|
592
|
+
}, async ({ project: e, flag: n, environment: i, enabled: o }) => f(async () => (await t.api.request("POST", `${r(e, n)}/environments/${a(i)}/toggle`, { body: { enabled: o } }), u({
|
|
593
|
+
project: e,
|
|
594
|
+
flag: n,
|
|
595
|
+
environment: i,
|
|
596
|
+
enabled: o
|
|
597
|
+
})))), e.registerTool("update_flag_environment_config", {
|
|
598
|
+
title: "Update flag environment config",
|
|
599
|
+
description: "Update a flag's per-environment serving config: defaultVariationId (served on fallthrough, required), strategy (SingleVariation | PercentageRollout | TargetedRollout — only SingleVariation is currently supported at the environment level), and prerequisites. Percentage rollouts are configured on targeting rules via update_targeting.",
|
|
600
|
+
inputSchema: n.object({
|
|
601
|
+
project: T,
|
|
602
|
+
flag: E,
|
|
603
|
+
environment: D,
|
|
604
|
+
defaultVariationId: n.string().describe("Variation id to serve on fallthrough — required by the API"),
|
|
605
|
+
strategy: I.optional().describe("Serving strategy; only SingleVariation is currently supported here"),
|
|
606
|
+
prerequisites: n.array(n.object({
|
|
607
|
+
prerequisiteFlagKey: n.string(),
|
|
608
|
+
expectedVariationKey: n.string()
|
|
609
|
+
})).optional().describe("Flags that must evaluate to the expected variation before this flag serves. Omit to leave existing prerequisites untouched; pass a list (possibly empty) to replace them wholesale.")
|
|
610
|
+
}),
|
|
611
|
+
outputSchema: S({
|
|
612
|
+
project: { type: "string" },
|
|
613
|
+
flag: { type: "string" },
|
|
614
|
+
environment: { type: "string" },
|
|
615
|
+
defaultVariationId: { type: "string" },
|
|
616
|
+
strategy: { type: "string" },
|
|
617
|
+
prerequisites: {
|
|
618
|
+
type: "array",
|
|
619
|
+
items: w({
|
|
620
|
+
prerequisiteFlagKey: { type: "string" },
|
|
621
|
+
expectedVariationKey: { type: "string" }
|
|
622
|
+
})
|
|
623
|
+
}
|
|
624
|
+
}, [
|
|
625
|
+
"project",
|
|
626
|
+
"flag",
|
|
627
|
+
"environment",
|
|
628
|
+
"defaultVariationId"
|
|
629
|
+
]),
|
|
630
|
+
annotations: { destructiveHint: !0 }
|
|
631
|
+
}, async ({ project: e, flag: n, environment: i, ...o }) => f(async () => (await t.api.request("PUT", `${r(e, n)}/environments/${a(i)}`, { body: o }), u({
|
|
632
|
+
project: e,
|
|
633
|
+
flag: n,
|
|
634
|
+
environment: i,
|
|
635
|
+
...o
|
|
636
|
+
})))), e.registerTool("flag_status", {
|
|
637
|
+
title: "Flag status across environments",
|
|
638
|
+
description: "Compact cross-environment view of one flag: enabled state, strategy, default variation, and prerequisites per environment.",
|
|
639
|
+
inputSchema: n.object({
|
|
640
|
+
project: T,
|
|
641
|
+
flag: E
|
|
642
|
+
}),
|
|
643
|
+
outputSchema: S({
|
|
644
|
+
key: g("string"),
|
|
645
|
+
name: g("string"),
|
|
646
|
+
type: g("string"),
|
|
647
|
+
archived: { type: "boolean" },
|
|
648
|
+
updatedAt: g("string"),
|
|
649
|
+
environments: {
|
|
650
|
+
type: "array",
|
|
651
|
+
items: w({
|
|
652
|
+
environment: g("string"),
|
|
653
|
+
enabled: { type: "boolean" },
|
|
654
|
+
strategy: g("string"),
|
|
655
|
+
defaultVariation: {
|
|
656
|
+
...g("string"),
|
|
657
|
+
description: "Variation key served on fallthrough"
|
|
658
|
+
},
|
|
659
|
+
prerequisites: { type: "array" }
|
|
660
|
+
})
|
|
661
|
+
}
|
|
662
|
+
}, [
|
|
663
|
+
"key",
|
|
664
|
+
"name",
|
|
665
|
+
"type",
|
|
666
|
+
"archived",
|
|
667
|
+
"updatedAt",
|
|
668
|
+
"environments"
|
|
669
|
+
]),
|
|
670
|
+
annotations: { readOnlyHint: !0 }
|
|
671
|
+
}, async ({ project: e, flag: n }) => f(async () => {
|
|
672
|
+
let [i, a] = await Promise.all([t.api.request("GET", r(e, n)), t.api.request("GET", `${r(e, n)}/environments`)]), o = (e) => (i.variations ?? []).find((t) => t.id === e)?.key ?? e ?? null;
|
|
673
|
+
return u({
|
|
674
|
+
key: i.key ?? null,
|
|
675
|
+
name: i.name ?? null,
|
|
676
|
+
type: i.type ?? null,
|
|
677
|
+
archived: i.isArchived ?? !1,
|
|
678
|
+
updatedAt: i.updatedAt ?? null,
|
|
679
|
+
environments: a.map((e) => ({
|
|
680
|
+
environment: e.environmentKey ?? null,
|
|
681
|
+
enabled: e.isEnabled ?? !1,
|
|
682
|
+
strategy: e.strategy ?? null,
|
|
683
|
+
defaultVariation: o(e.defaultVariationId),
|
|
684
|
+
prerequisites: e.prerequisites ?? []
|
|
685
|
+
}))
|
|
686
|
+
});
|
|
687
|
+
}));
|
|
688
|
+
}
|
|
689
|
+
//#endregion
|
|
690
|
+
//#region src/tools/targeting.ts
|
|
691
|
+
var ne = n.object({
|
|
692
|
+
attribute: n.string().describe("Context attribute name, e.g. plan, country, user_id"),
|
|
693
|
+
operator: n.string().describe("OperatorType member name, case-sensitive — e.g. Equals, NotEquals, Contains, In, GreaterThan"),
|
|
694
|
+
values: n.array(n.string()),
|
|
695
|
+
negate: n.boolean().optional().default(!1)
|
|
696
|
+
}), L = n.object({
|
|
697
|
+
description: n.string().optional(),
|
|
698
|
+
variationId: n.string().optional().describe("Variation served when the rule matches"),
|
|
699
|
+
rolloutPercentage: n.number().int().min(0).max(100).optional().describe("Percentage rollout for this rule"),
|
|
700
|
+
userSegmentId: n.string().optional().describe("Match users in this segment (see list_segments)"),
|
|
701
|
+
conditionGroups: n.array(n.object({
|
|
702
|
+
operator: n.enum(["And", "Or"]).describe("How conditions inside this group combine"),
|
|
703
|
+
conditions: n.array(ne)
|
|
704
|
+
})).optional().describe("Groups are ANDed together; conditions inside a group use the group operator")
|
|
705
|
+
});
|
|
706
|
+
function R(e, t) {
|
|
707
|
+
let r = (e, n, r) => `/api/v1/orgs/${a(t.org)}/projects/${a(e)}/flags/${a(n)}/environments/${a(r)}/targeting`;
|
|
708
|
+
e.registerTool("get_targeting", {
|
|
709
|
+
title: "Get targeting rules",
|
|
710
|
+
description: "Get a flag's current targeting configuration (enabled flag + ordered rules) in one environment.",
|
|
711
|
+
inputSchema: n.object({
|
|
712
|
+
project: T,
|
|
713
|
+
flag: E,
|
|
714
|
+
environment: D
|
|
715
|
+
}),
|
|
716
|
+
outputSchema: x("GET /api/v1/orgs/{org}/projects/{project}/flags/{flag}/environments/{env}/targeting"),
|
|
717
|
+
annotations: { readOnlyHint: !0 }
|
|
718
|
+
}, async ({ project: e, flag: n, environment: i }) => f(async () => u(await t.api.request("GET", r(e, n, i))))), e.registerTool("update_targeting", {
|
|
719
|
+
title: "Replace targeting rules",
|
|
720
|
+
description: "REPLACE all targeting rules of a flag in one environment (full PUT — rules not included are removed). Rules are evaluated in order. Get the current rules first with get_targeting.",
|
|
721
|
+
inputSchema: n.object({
|
|
722
|
+
project: T,
|
|
723
|
+
flag: E,
|
|
724
|
+
environment: D,
|
|
725
|
+
rules: n.array(L).describe("The complete ordered rule list; rules left out are deleted")
|
|
726
|
+
}),
|
|
727
|
+
outputSchema: S({
|
|
728
|
+
project: { type: "string" },
|
|
729
|
+
flag: { type: "string" },
|
|
730
|
+
environment: { type: "string" },
|
|
731
|
+
rulesReplaced: { type: "integer" }
|
|
732
|
+
}, [
|
|
733
|
+
"project",
|
|
734
|
+
"flag",
|
|
735
|
+
"environment",
|
|
736
|
+
"rulesReplaced"
|
|
737
|
+
]),
|
|
738
|
+
annotations: { destructiveHint: !0 }
|
|
739
|
+
}, async ({ project: e, flag: n, environment: i, rules: a }) => f(async () => (await t.api.request("PUT", r(e, n, i), { body: { rules: a } }), u({
|
|
740
|
+
project: e,
|
|
741
|
+
flag: n,
|
|
742
|
+
environment: i,
|
|
743
|
+
rulesReplaced: a.length
|
|
744
|
+
}))));
|
|
745
|
+
}
|
|
746
|
+
//#endregion
|
|
747
|
+
//#region src/tools/variations.ts
|
|
748
|
+
function z(e, t) {
|
|
749
|
+
e.registerTool("manage_variation", {
|
|
750
|
+
title: "Add, update, or remove a variation",
|
|
751
|
+
description: "Manage a flag's variations. action=add requires key + value; action=update/remove require variationId (get ids via get_flag). Removing a variation fails with VARIATION_HAS_DEPENDENTS if other flags depend on it.",
|
|
752
|
+
inputSchema: n.object({
|
|
753
|
+
project: T,
|
|
754
|
+
flag: E,
|
|
755
|
+
action: n.enum([
|
|
756
|
+
"add",
|
|
757
|
+
"update",
|
|
758
|
+
"remove"
|
|
759
|
+
]).describe("add needs key + value; update and remove need variationId"),
|
|
760
|
+
variationId: n.string().optional().describe("Required for update/remove"),
|
|
761
|
+
key: n.string().optional().describe("Required for add; immutable afterwards"),
|
|
762
|
+
name: n.string().optional().describe("Display name"),
|
|
763
|
+
value: n.string().optional().describe("Variation value serialized as a string; required for add"),
|
|
764
|
+
description: n.string().optional().describe("What this variation means")
|
|
765
|
+
}),
|
|
766
|
+
outputSchema: C(v("POST /api/v1/orgs/{org}/projects/{project}/flags/{flag}/variations"), w({ updated: {
|
|
767
|
+
type: "string",
|
|
768
|
+
description: "Id of the updated variation"
|
|
769
|
+
} }, ["updated"]), w({ removed: {
|
|
770
|
+
type: "string",
|
|
771
|
+
description: "Id of the removed variation"
|
|
772
|
+
} }, ["removed"])),
|
|
773
|
+
annotations: { destructiveHint: !0 }
|
|
774
|
+
}, async ({ project: e, flag: n, action: r, variationId: i, key: o, name: s, value: c, description: l }) => f(async () => {
|
|
775
|
+
let f = `/api/v1/orgs/${a(t.org)}/projects/${a(e)}/flags/${a(n)}/variations`;
|
|
776
|
+
return r === "add" ? !o || c === void 0 ? d(/* @__PURE__ */ Error("action=add requires key and value")) : u(await t.api.request("POST", f, { body: {
|
|
777
|
+
key: o,
|
|
778
|
+
name: s,
|
|
779
|
+
value: c,
|
|
780
|
+
description: l
|
|
781
|
+
} })) : i ? r === "update" ? (await t.api.request("PUT", `${f}/${a(i)}`, { body: {
|
|
782
|
+
name: s,
|
|
783
|
+
value: c,
|
|
784
|
+
description: l
|
|
785
|
+
} }), u({ updated: i })) : (await t.api.request("DELETE", `${f}/${a(i)}`), u({ removed: i })) : d(/* @__PURE__ */ Error(`action=${r} requires variationId (see get_flag for ids)`));
|
|
786
|
+
}));
|
|
787
|
+
}
|
|
788
|
+
//#endregion
|
|
789
|
+
//#region src/tools/webhooks.ts
|
|
790
|
+
var B = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, V = "Requires an Admin token. A 404 on every webhook call means webhooks are not enabled for the organization.";
|
|
791
|
+
function H(e, t) {
|
|
792
|
+
let r = `/api/v1/orgs/${a(t.org)}/webhooks`, i = (e) => `${r}/${a(e)}`;
|
|
793
|
+
async function o(e) {
|
|
794
|
+
let n = [];
|
|
795
|
+
for (let r of e) {
|
|
796
|
+
if (B.test(r)) {
|
|
797
|
+
n.push(r);
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
let e = await t.api.request("GET", `/api/v1/orgs/${a(t.org)}/projects/${a(r)}`);
|
|
801
|
+
if (!e?.id) throw Error(`Project "${r}" resolved without an id`);
|
|
802
|
+
n.push(e.id);
|
|
803
|
+
}
|
|
804
|
+
return n;
|
|
805
|
+
}
|
|
806
|
+
async function s(e) {
|
|
807
|
+
let n = e.map((e) => {
|
|
808
|
+
if (B.test(e)) return { id: e };
|
|
809
|
+
let t = e.indexOf("/"), n = e.slice(0, t), r = e.slice(t + 1);
|
|
810
|
+
if (t < 0 || !n || !r) throw Error(`Environment "${e}" needs its project: pass "<project>/<environment>", e.g. "web/production", or the environment's id.`);
|
|
811
|
+
return {
|
|
812
|
+
project: n,
|
|
813
|
+
env: r
|
|
814
|
+
};
|
|
815
|
+
}), r = [];
|
|
816
|
+
for (let e of n) {
|
|
817
|
+
if ("id" in e) {
|
|
818
|
+
r.push(e.id);
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
821
|
+
let n = await t.api.request("GET", `/api/v1/orgs/${a(t.org)}/projects/${a(e.project)}/environments/${a(e.env)}`);
|
|
822
|
+
if (!n?.id) throw Error(`Environment "${e.project}/${e.env}" resolved without an id`);
|
|
823
|
+
r.push(n.id);
|
|
824
|
+
}
|
|
825
|
+
return r;
|
|
826
|
+
}
|
|
827
|
+
e.registerTool("list_webhooks", {
|
|
828
|
+
title: "List webhook subscriptions",
|
|
829
|
+
description: "List the organization's outbound webhook subscriptions (url, event/project/environment filters, enabled state, failure count, signing secret ids — never secret values), plus availableEventTypes: the event types a subscription can filter on. Empty filter lists mean \"all\". Paginated via cursor. " + V,
|
|
830
|
+
inputSchema: n.object({ ...O }),
|
|
831
|
+
outputSchema: ee("GET /api/v1/orgs/{org}/webhooks", { availableEventTypes: {
|
|
832
|
+
type: "array",
|
|
833
|
+
items: { type: "string" },
|
|
834
|
+
description: "Event types a subscription can filter on"
|
|
835
|
+
} }),
|
|
836
|
+
annotations: { readOnlyHint: !0 }
|
|
837
|
+
}, async ({ limit: e, cursor: n }) => f(async () => {
|
|
838
|
+
let [i, a] = await Promise.all([t.api.request("GET", r, { query: {
|
|
839
|
+
limit: e,
|
|
840
|
+
cursor: n
|
|
841
|
+
} }), t.api.request("GET", `${r}/event-types`)]);
|
|
842
|
+
return u({
|
|
843
|
+
...i,
|
|
844
|
+
availableEventTypes: a
|
|
845
|
+
});
|
|
846
|
+
})), e.registerTool("list_webhook_deliveries", {
|
|
847
|
+
title: "List webhook deliveries",
|
|
848
|
+
description: "List one subscription's delivery attempts, most recent first. status is Pending (awaiting an attempt, backing off, or in flight), Succeeded or DeadLettered; lastResponseStatusCode and lastError explain a failure. Paginated via cursor. " + V,
|
|
849
|
+
inputSchema: n.object({
|
|
850
|
+
id: n.string().describe("Subscription id (from list_webhooks)"),
|
|
851
|
+
...O
|
|
852
|
+
}),
|
|
853
|
+
outputSchema: x("GET /api/v1/orgs/{org}/webhooks/{id}/deliveries"),
|
|
854
|
+
annotations: { readOnlyHint: !0 }
|
|
855
|
+
}, async ({ id: e, limit: n, cursor: r }) => f(async () => u(await t.api.request("GET", `${i(e)}/deliveries`, { query: {
|
|
856
|
+
limit: n,
|
|
857
|
+
cursor: r
|
|
858
|
+
} })))), e.registerTool("manage_webhook", {
|
|
859
|
+
title: "Create, update, or delete a webhook subscription, or rotate its secret",
|
|
860
|
+
description: "action=create requires name + url and returns the signing secret. action=update/delete/rotate_secret/retire_secret require id. update changes only the fields you pass and keeps the rest; pass an empty list to widen that filter back to \"all\". projects take project keys or ids; environments take \"<project>/<environment>\" keys (e.g. \"web/production\") or ids. eventTypes come from list_webhooks → availableEventTypes. create and rotate_secret return a secret that is NEVER shown again: relay it to the user verbatim so they can store it. rotate_secret adds a new active secret while the old one keeps signing; retire_secret (secretId from list_webhooks) removes one and is refused for the last active secret. A project-restricted service token must list at least one project, all in its scope. " + V,
|
|
861
|
+
inputSchema: n.object({
|
|
862
|
+
action: n.enum([
|
|
863
|
+
"create",
|
|
864
|
+
"update",
|
|
865
|
+
"delete",
|
|
866
|
+
"rotate_secret",
|
|
867
|
+
"retire_secret"
|
|
868
|
+
]).describe("What to do; every action except create needs id"),
|
|
869
|
+
id: n.string().optional().describe("Subscription id; required for everything except create"),
|
|
870
|
+
name: n.string().optional().describe("Required for create"),
|
|
871
|
+
url: n.string().optional().describe("Receiver URL; required for create"),
|
|
872
|
+
eventTypes: n.array(n.string()).optional().describe("Event types to send; empty or omitted = all"),
|
|
873
|
+
projects: n.array(n.string()).optional().describe("Project keys or ids to cover; empty or omitted on create = all"),
|
|
874
|
+
environments: n.array(n.string()).optional().describe("\"<project>/<environment>\" keys or environment ids; empty or omitted on create = all"),
|
|
875
|
+
enabled: n.boolean().optional().describe("update only; subscriptions are created enabled"),
|
|
876
|
+
secretId: n.string().optional().describe("Required for retire_secret"),
|
|
877
|
+
idempotency_key: n.string().optional().describe("Idempotency-Key header for create / rotate_secret")
|
|
878
|
+
}),
|
|
879
|
+
outputSchema: C(v("POST /api/v1/orgs/{org}/webhooks"), w({
|
|
880
|
+
id: { type: "string" },
|
|
881
|
+
updated: { type: "boolean" },
|
|
882
|
+
name: g("string"),
|
|
883
|
+
url: g("string"),
|
|
884
|
+
eventTypes: {
|
|
885
|
+
type: ["array", "null"],
|
|
886
|
+
items: { type: "string" }
|
|
887
|
+
},
|
|
888
|
+
projectIds: {
|
|
889
|
+
type: ["array", "null"],
|
|
890
|
+
items: { type: "string" }
|
|
891
|
+
},
|
|
892
|
+
environmentIds: {
|
|
893
|
+
type: ["array", "null"],
|
|
894
|
+
items: { type: "string" }
|
|
895
|
+
},
|
|
896
|
+
isEnabled: g("boolean")
|
|
897
|
+
}, ["id", "updated"]), w({ deleted: { type: "string" } }, ["deleted"]), v("POST /api/v1/orgs/{org}/webhooks/{id}/secrets"), w({
|
|
898
|
+
id: { type: "string" },
|
|
899
|
+
retiredSecretId: { type: "string" }
|
|
900
|
+
}, ["id", "retiredSecretId"])),
|
|
901
|
+
annotations: { destructiveHint: !0 }
|
|
902
|
+
}, async ({ action: e, id: n, name: c, url: l, eventTypes: p, projects: m, environments: h, enabled: g, secretId: _, idempotency_key: v }) => f(async () => {
|
|
903
|
+
if (e === "create") {
|
|
904
|
+
if (!c || !l) return d(/* @__PURE__ */ Error("action=create requires name and url"));
|
|
905
|
+
let e = h && await s(h), n = m && await o(m);
|
|
906
|
+
return u(await t.api.request("POST", r, {
|
|
907
|
+
body: {
|
|
908
|
+
name: c,
|
|
909
|
+
url: l,
|
|
910
|
+
provider: "GenericHttp",
|
|
911
|
+
eventTypes: p,
|
|
912
|
+
projectIds: n,
|
|
913
|
+
environmentIds: e
|
|
914
|
+
},
|
|
915
|
+
idempotencyKey: v
|
|
916
|
+
}));
|
|
917
|
+
}
|
|
918
|
+
if (!n) return d(/* @__PURE__ */ Error(`action=${e} requires id (see list_webhooks)`));
|
|
919
|
+
if (e === "update") {
|
|
920
|
+
let e = h && await s(h), r = m && await o(m), a = await t.api.request("GET", i(n)), d = {
|
|
921
|
+
name: c ?? a.name,
|
|
922
|
+
url: l ?? a.url,
|
|
923
|
+
eventTypes: p ?? a.eventTypes,
|
|
924
|
+
projectIds: r ?? a.projectIds,
|
|
925
|
+
environmentIds: e ?? a.environmentIds,
|
|
926
|
+
isEnabled: g ?? a.isEnabled
|
|
927
|
+
};
|
|
928
|
+
return await t.api.request("PUT", i(n), { body: d }), u({
|
|
929
|
+
id: n,
|
|
930
|
+
updated: !0,
|
|
931
|
+
...d
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
return e === "delete" ? (await t.api.request("DELETE", i(n)), u({ deleted: n })) : e === "rotate_secret" ? u(await t.api.request("POST", `${i(n)}/secrets`, { idempotencyKey: v })) : _ ? (await t.api.request("DELETE", `${i(n)}/secrets/${a(_)}`), u({
|
|
935
|
+
id: n,
|
|
936
|
+
retiredSecretId: _
|
|
937
|
+
})) : d(/* @__PURE__ */ Error("action=retire_secret requires secretId (see list_webhooks)"));
|
|
938
|
+
})), e.registerTool("deliver_webhook", {
|
|
939
|
+
title: "Send a test event or redeliver",
|
|
940
|
+
description: "action=test queues one synthetic flag.toggled event to this subscription alone; action=redeliver (deliveryId from list_webhook_deliveries) re-sends a finished delivery, restarting its retry schedule. Delivery is asynchronous: read list_webhook_deliveries for the outcome. Both are refused for a disabled subscription, and redeliver for a delivery that is still Pending. " + V,
|
|
941
|
+
inputSchema: n.object({
|
|
942
|
+
action: n.enum(["test", "redeliver"]).describe("test sends a synthetic event; redeliver re-sends deliveryId"),
|
|
943
|
+
id: n.string().describe("Subscription id (from list_webhooks)"),
|
|
944
|
+
deliveryId: n.string().optional().describe("Required for redeliver")
|
|
945
|
+
}),
|
|
946
|
+
outputSchema: C(v("POST /api/v1/orgs/{org}/webhooks/{id}/test"), w({
|
|
947
|
+
id: { type: "string" },
|
|
948
|
+
redelivered: { type: "string" }
|
|
949
|
+
}, ["id", "redelivered"])),
|
|
950
|
+
annotations: {
|
|
951
|
+
destructiveHint: !1,
|
|
952
|
+
openWorldHint: !0
|
|
953
|
+
}
|
|
954
|
+
}, async ({ action: e, id: n, deliveryId: r }) => f(async () => e === "test" ? u(await t.api.request("POST", `${i(n)}/test`)) : r ? (await t.api.request("POST", `${i(n)}/deliveries/${a(r)}/redeliver`), u({
|
|
955
|
+
id: n,
|
|
956
|
+
redelivered: r
|
|
957
|
+
})) : d(/* @__PURE__ */ Error("action=redeliver requires deliveryId (see list_webhook_deliveries)"))));
|
|
958
|
+
}
|
|
959
|
+
//#endregion
|
|
960
|
+
//#region src/tools/workflows/stale-flags.ts
|
|
961
|
+
var U = n.enum(["dead", "stale"]), W = 50;
|
|
962
|
+
function G(e, t) {
|
|
963
|
+
e.registerTool("find_stale_flags", {
|
|
964
|
+
title: "Find stale flags",
|
|
965
|
+
description: `Find flags that look ready for code cleanup: not updated in N days AND either enabled in every environment (verify rollout is complete before removing — per-rule percentage ramps are not inspected) or disabled in every environment (dead — remove flag and code path). A flag whose expiry date (set_flag_expiry) has passed is always a candidate, however recently it was edited; if it is on in some environments and off in others its reason is past-expiry, meaning the owner has to decide which way to fold it. A project can give its temporary flags a default lifetime, so a flag with no date of its own can still be past expiry. Every result carries expiresAtUtc (the date set on the flag), effectiveExpiresAtUtc (the date in effect, from the flag or its project default), expirySource (explicit, project-default, or null), expired and owner (null when unowned). Flags marked permanent (kill switches, ops toggles) are never listed. Pass owner to see one person's stale flags ("me" for your own) or "none" for the unowned ones. Every result also carries blockedBy: the live flags that still list it as a prerequisite, which have to be removed and archived before it ([] when none). blockedBy is null when the server did not classify the flag as a removal candidate, which means unknown, not unblocked. A flag that other live flags list as a prerequisite has to be removed LAST, dependents first: archiving it while a dependent still names it makes that dependent fail its prerequisite check and serve its off variation, so archive_flag refuses it with FLAG_HAS_DEPENDENTS. Checks at most ${W} candidates per call, expired flags first.`,
|
|
966
|
+
inputSchema: n.object({
|
|
967
|
+
project: T,
|
|
968
|
+
days: n.number().int().min(1).optional().default(30).describe("Minimum age in days since last update"),
|
|
969
|
+
owner: M
|
|
970
|
+
}),
|
|
971
|
+
outputSchema: S({
|
|
972
|
+
project: { type: "string" },
|
|
973
|
+
olderThanDays: { type: "integer" },
|
|
974
|
+
owner: { type: "string" },
|
|
975
|
+
checked: { type: "integer" },
|
|
976
|
+
truncated: { type: "boolean" },
|
|
977
|
+
note: { type: "string" },
|
|
978
|
+
blockedByNote: { type: "string" },
|
|
979
|
+
stale: {
|
|
980
|
+
type: "array",
|
|
981
|
+
items: w({
|
|
982
|
+
key: { type: "string" },
|
|
983
|
+
name: { type: "string" },
|
|
984
|
+
updatedAt: { type: "string" },
|
|
985
|
+
expiresAtUtc: g("string"),
|
|
986
|
+
effectiveExpiresAtUtc: g("string"),
|
|
987
|
+
expirySource: {
|
|
988
|
+
...g("string"),
|
|
989
|
+
description: "explicit, project-default, or null"
|
|
990
|
+
},
|
|
991
|
+
expired: { type: "boolean" },
|
|
992
|
+
owner: { anyOf: [_("PublicFlagOwner"), { type: "null" }] },
|
|
993
|
+
reason: {
|
|
994
|
+
type: "string",
|
|
995
|
+
description: "enabled-everywhere, disabled-everywhere or past-expiry"
|
|
996
|
+
},
|
|
997
|
+
environments: { type: "integer" },
|
|
998
|
+
blockedBy: {
|
|
999
|
+
type: ["array", "null"],
|
|
1000
|
+
items: { type: "string" },
|
|
1001
|
+
description: "null means unknown, not unblocked"
|
|
1002
|
+
}
|
|
1003
|
+
})
|
|
1004
|
+
}
|
|
1005
|
+
}, [
|
|
1006
|
+
"project",
|
|
1007
|
+
"olderThanDays",
|
|
1008
|
+
"checked",
|
|
1009
|
+
"truncated",
|
|
1010
|
+
"stale"
|
|
1011
|
+
]),
|
|
1012
|
+
annotations: { readOnlyHint: !0 }
|
|
1013
|
+
}, async ({ project: e, days: n, owner: r }) => f(async () => {
|
|
1014
|
+
let o = await N(t, r), s = `/api/v1/orgs/${a(t.org)}/projects/${a(e)}/flags`, c = Date.now(), l = c - n * 24 * 60 * 60 * 1e3, d = (e) => e.effectiveExpiresAtUtc ?? e.expiresAtUtc ?? null, f = (e) => e.expirySource ?? (e.expiresAtUtc ? "explicit" : null), p = (e) => {
|
|
1015
|
+
let t = d(e);
|
|
1016
|
+
return !!t && new Date(t).getTime() <= c;
|
|
1017
|
+
}, m = [], h;
|
|
1018
|
+
do {
|
|
1019
|
+
let e = await t.api.request("GET", s, { query: {
|
|
1020
|
+
archived: !1,
|
|
1021
|
+
limit: 100,
|
|
1022
|
+
cursor: h,
|
|
1023
|
+
owner: o,
|
|
1024
|
+
permanent: !1
|
|
1025
|
+
} });
|
|
1026
|
+
m.push(...(e.items ?? []).filter((e) => !!e.key && !!e.name && !!e.updatedAt && (new Date(e.updatedAt).getTime() < l || p(e)))), h = e.next_cursor ?? void 0;
|
|
1027
|
+
} while (h);
|
|
1028
|
+
m.sort((e, t) => Number(p(t)) - Number(p(e)));
|
|
1029
|
+
let g = m.length > W, _ = m.slice(0, W), v = [];
|
|
1030
|
+
for (let e of _) {
|
|
1031
|
+
let n = await t.api.request("GET", `${s}/${a(e.key)}/environments`);
|
|
1032
|
+
if (n.length === 0) continue;
|
|
1033
|
+
let r = p(e), i = n.every((e) => e.isEnabled), o = n.every((e) => !e.isEnabled);
|
|
1034
|
+
(i || o || r) && v.push({
|
|
1035
|
+
key: e.key,
|
|
1036
|
+
name: e.name,
|
|
1037
|
+
updatedAt: e.updatedAt,
|
|
1038
|
+
expiresAtUtc: e.expiresAtUtc ?? null,
|
|
1039
|
+
effectiveExpiresAtUtc: d(e),
|
|
1040
|
+
expirySource: f(e),
|
|
1041
|
+
expired: r,
|
|
1042
|
+
owner: e.owner ?? null,
|
|
1043
|
+
reason: i ? "enabled-everywhere" : o ? "disabled-everywhere" : "past-expiry",
|
|
1044
|
+
environments: n.length,
|
|
1045
|
+
blockedBy: null
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
let y;
|
|
1049
|
+
if (v.length > 0) try {
|
|
1050
|
+
let e = /* @__PURE__ */ new Map(), n;
|
|
1051
|
+
do {
|
|
1052
|
+
let r = await t.api.request("GET", `${s}/removal-candidates`, { query: {
|
|
1053
|
+
staleness: "stale",
|
|
1054
|
+
limit: 100,
|
|
1055
|
+
cursor: n
|
|
1056
|
+
} });
|
|
1057
|
+
for (let t of r.items ?? []) t.key && t.blockedBy && e.set(t.key, t.blockedBy);
|
|
1058
|
+
n = r.next_cursor ?? void 0;
|
|
1059
|
+
} while (n);
|
|
1060
|
+
for (let t of v) t.blockedBy = e.get(t.key) ?? null;
|
|
1061
|
+
} catch (e) {
|
|
1062
|
+
if (!(e instanceof i)) throw e;
|
|
1063
|
+
y = `Could not read removal-candidates (${e.status} ${e.envelope.error}), so blockedBy is unknown for every flag. archive_flag still refuses any flag that a live flag depends on (FLAG_HAS_DEPENDENTS).`;
|
|
1064
|
+
}
|
|
1065
|
+
return u({
|
|
1066
|
+
project: e,
|
|
1067
|
+
olderThanDays: n,
|
|
1068
|
+
...o ? { owner: o } : {},
|
|
1069
|
+
checked: _.length,
|
|
1070
|
+
truncated: g,
|
|
1071
|
+
...g ? { note: `Only the first ${W} of ${m.length} candidates were checked.` } : {},
|
|
1072
|
+
...y ? { blockedByNote: y } : {},
|
|
1073
|
+
stale: v
|
|
1074
|
+
});
|
|
1075
|
+
})), e.registerTool("list_removal_candidates", {
|
|
1076
|
+
title: "List removal candidates",
|
|
1077
|
+
description: "List the flags the server is confident can be removed from code, the same list the Featureflip flag cleanup GitHub Action works from. Each item has key, reason, status (Dead or Stale), treatment (true = keep the on-branch, false = keep the off-branch) and blockedBy: the live flags that still list it as a prerequisite. Only remove a flag whose blockedBy is empty. A flag that other live flags list as a prerequisite has to be removed LAST, dependents first: archiving it while a dependent still names it makes that dependent fail its prerequisite check and serve its off variation, so archive_flag refuses it with FLAG_HAS_DEPENDENTS. staleness \"dead\" (the default) returns only dead flags, \"stale\" adds stale ones. Paginated via cursor.",
|
|
1078
|
+
inputSchema: n.object({
|
|
1079
|
+
project: T,
|
|
1080
|
+
staleness: U.optional().describe("Minimum staleness tier: dead (default) or stale"),
|
|
1081
|
+
...O
|
|
1082
|
+
}),
|
|
1083
|
+
outputSchema: x("GET /api/v1/orgs/{org}/projects/{project}/flags/removal-candidates"),
|
|
1084
|
+
annotations: { readOnlyHint: !0 }
|
|
1085
|
+
}, async ({ project: e, ...n }) => f(async () => u(await t.api.request("GET", `/api/v1/orgs/${a(t.org)}/projects/${a(e)}/flags/removal-candidates`, { query: n }))));
|
|
1086
|
+
}
|
|
1087
|
+
//#endregion
|
|
1088
|
+
//#region src/tools/workflows/snippets.ts
|
|
1089
|
+
var K = [
|
|
1090
|
+
"js",
|
|
1091
|
+
"node",
|
|
1092
|
+
"browser",
|
|
1093
|
+
"react",
|
|
1094
|
+
"python",
|
|
1095
|
+
"csharp",
|
|
1096
|
+
"java",
|
|
1097
|
+
"go",
|
|
1098
|
+
"php",
|
|
1099
|
+
"ruby",
|
|
1100
|
+
"swift"
|
|
1101
|
+
], q = [
|
|
1102
|
+
"browser",
|
|
1103
|
+
"react",
|
|
1104
|
+
"swift"
|
|
1105
|
+
];
|
|
1106
|
+
function J(e) {
|
|
1107
|
+
return q.includes(e);
|
|
1108
|
+
}
|
|
1109
|
+
var Y = "Uses a SERVER-side SDK key (Featureflip dashboard → environment → SDK keys).", X = "Uses a CLIENT-side SDK key (safe to ship to browsers/apps).";
|
|
1110
|
+
function re(e, t) {
|
|
1111
|
+
switch (e) {
|
|
1112
|
+
case "js": return {
|
|
1113
|
+
install: "npm install @featureflip/js",
|
|
1114
|
+
code: `import { FeatureflipClient, createNodePlatform } from '@featureflip/js';
|
|
1115
|
+
|
|
1116
|
+
const client = FeatureflipClient.get(
|
|
1117
|
+
{ sdkKey: process.env.FEATUREFLIP_SDK_KEY!, baseUrl: 'https://eval.featureflip.io' },
|
|
1118
|
+
createNodePlatform(),
|
|
1119
|
+
);
|
|
1120
|
+
await client.waitForInitialization();
|
|
1121
|
+
const enabled = client.boolVariation('${t}', { user_id: userId }, false);
|
|
1122
|
+
if (enabled) {
|
|
1123
|
+
// new code path
|
|
1124
|
+
}`,
|
|
1125
|
+
notes: Y
|
|
1126
|
+
};
|
|
1127
|
+
case "node": return {
|
|
1128
|
+
install: "npm install @featureflip/node",
|
|
1129
|
+
code: `import { FeatureflipClient } from '@featureflip/node';
|
|
1130
|
+
|
|
1131
|
+
const client = await FeatureflipClient.create({ sdkKey: process.env.FEATUREFLIP_SDK_KEY! });
|
|
1132
|
+
const enabled = client.boolVariation('${t}', { user_id: userId }, false);
|
|
1133
|
+
if (enabled) {
|
|
1134
|
+
// new code path
|
|
1135
|
+
}`,
|
|
1136
|
+
notes: Y
|
|
1137
|
+
};
|
|
1138
|
+
case "browser": return {
|
|
1139
|
+
install: "npm install @featureflip/browser",
|
|
1140
|
+
code: `import { FeatureflipClient } from '@featureflip/browser';
|
|
1141
|
+
|
|
1142
|
+
const client = FeatureflipClient.get({ clientKey: 'YOUR_CLIENT_SDK_KEY' });
|
|
1143
|
+
await client.initialize();
|
|
1144
|
+
const enabled = client.boolVariation('${t}', false);
|
|
1145
|
+
if (enabled) {
|
|
1146
|
+
// new code path
|
|
1147
|
+
}`,
|
|
1148
|
+
notes: X
|
|
1149
|
+
};
|
|
1150
|
+
case "react": return {
|
|
1151
|
+
install: "npm install @featureflip/react @featureflip/browser",
|
|
1152
|
+
code: `import { FeatureflipProvider, useFeatureFlag } from '@featureflip/react';
|
|
1153
|
+
|
|
1154
|
+
// In your app root:
|
|
1155
|
+
// <FeatureflipProvider clientKey="YOUR_CLIENT_SDK_KEY">...</FeatureflipProvider>
|
|
1156
|
+
|
|
1157
|
+
function MyComponent() {
|
|
1158
|
+
const enabled = useFeatureFlag('${t}', false);
|
|
1159
|
+
return enabled ? <NewExperience /> : <CurrentExperience />;
|
|
1160
|
+
}`,
|
|
1161
|
+
notes: X
|
|
1162
|
+
};
|
|
1163
|
+
case "python": return {
|
|
1164
|
+
install: "pip install featureflip",
|
|
1165
|
+
code: `from featureflip import FeatureflipClient
|
|
1166
|
+
|
|
1167
|
+
client = FeatureflipClient(sdk_key=os.environ["FEATUREFLIP_SDK_KEY"])
|
|
1168
|
+
enabled = client.variation("${t}", {"user_id": user_id}, default=False)
|
|
1169
|
+
if enabled:
|
|
1170
|
+
# new code path
|
|
1171
|
+
...`,
|
|
1172
|
+
notes: Y
|
|
1173
|
+
};
|
|
1174
|
+
case "csharp": return {
|
|
1175
|
+
install: "dotnet add package Featureflip.Client",
|
|
1176
|
+
code: `using Featureflip.Client;
|
|
1177
|
+
|
|
1178
|
+
var client = FeatureflipClient.Get(Environment.GetEnvironmentVariable("FEATUREFLIP_SDK_KEY")!);
|
|
1179
|
+
var enabled = client.BoolVariation("${t}",
|
|
1180
|
+
new EvaluationContext { UserId = userId }, false);
|
|
1181
|
+
if (enabled)
|
|
1182
|
+
{
|
|
1183
|
+
// new code path
|
|
1184
|
+
}`,
|
|
1185
|
+
notes: Y
|
|
1186
|
+
};
|
|
1187
|
+
case "java": return {
|
|
1188
|
+
install: "implementation 'io.featureflip:featureflip-java:2.0.0'",
|
|
1189
|
+
code: `import io.featureflip.client.FeatureflipClient;
|
|
1190
|
+
import io.featureflip.client.EvaluationContext;
|
|
1191
|
+
|
|
1192
|
+
FeatureflipClient client = FeatureflipClient.get(System.getenv("FEATUREFLIP_SDK_KEY"));
|
|
1193
|
+
client.waitForInitialization();
|
|
1194
|
+
boolean enabled = client.boolVariation("${t}",
|
|
1195
|
+
EvaluationContext.of(userId), false);
|
|
1196
|
+
if (enabled) {
|
|
1197
|
+
// new code path
|
|
1198
|
+
}`,
|
|
1199
|
+
notes: Y
|
|
1200
|
+
};
|
|
1201
|
+
case "go": return {
|
|
1202
|
+
install: "go get github.com/canopy-labs/featureflip-go/v2",
|
|
1203
|
+
code: `import featureflip "github.com/canopy-labs/featureflip-go/v2"
|
|
1204
|
+
|
|
1205
|
+
client, err := featureflip.Get(os.Getenv("FEATUREFLIP_SDK_KEY"))
|
|
1206
|
+
if err != nil {
|
|
1207
|
+
log.Fatal(err)
|
|
1208
|
+
}
|
|
1209
|
+
defer client.Close()
|
|
1210
|
+
|
|
1211
|
+
ctx := featureflip.EvaluationContext{UserID: userID}
|
|
1212
|
+
if client.BoolVariation("${t}", ctx, false) {
|
|
1213
|
+
// new code path
|
|
1214
|
+
}`,
|
|
1215
|
+
notes: Y
|
|
1216
|
+
};
|
|
1217
|
+
case "php": return {
|
|
1218
|
+
install: "composer require featureflip/featureflip-php",
|
|
1219
|
+
code: `use Featureflip\\FeatureflipClient;
|
|
1220
|
+
use Featureflip\\Config;
|
|
1221
|
+
|
|
1222
|
+
$client = FeatureflipClient::get(getenv('FEATUREFLIP_SDK_KEY'), new Config(
|
|
1223
|
+
cache: $psrCache, httpClient: $psrHttpClient,
|
|
1224
|
+
requestFactory: $psrRequestFactory, streamFactory: $psrStreamFactory,
|
|
1225
|
+
));
|
|
1226
|
+
$enabled = $client->boolVariation('${t}', ['user_id' => $userId], false);
|
|
1227
|
+
if ($enabled) {
|
|
1228
|
+
// new code path
|
|
1229
|
+
}`,
|
|
1230
|
+
notes: Y + " Requires PSR-16 cache and PSR-18 HTTP implementations."
|
|
1231
|
+
};
|
|
1232
|
+
case "ruby": return {
|
|
1233
|
+
install: "gem \"featureflip\"",
|
|
1234
|
+
code: `require "featureflip"
|
|
1235
|
+
|
|
1236
|
+
client = Featureflip::Client.get(ENV.fetch("FEATUREFLIP_SDK_KEY"))
|
|
1237
|
+
enabled = client.bool_variation("${t}", { "user_id" => user_id }, false)
|
|
1238
|
+
if enabled
|
|
1239
|
+
# new code path
|
|
1240
|
+
end`,
|
|
1241
|
+
notes: Y
|
|
1242
|
+
};
|
|
1243
|
+
case "swift": return {
|
|
1244
|
+
install: ".package(url: \"https://github.com/canopy-labs/featureflip-swift\", from: \"2.0.0\")",
|
|
1245
|
+
code: `import Featureflip
|
|
1246
|
+
|
|
1247
|
+
let config = FeatureflipConfig(clientKey: "YOUR_CLIENT_SDK_KEY")
|
|
1248
|
+
let client = FeatureflipClient(config: config)
|
|
1249
|
+
await client.initialize()
|
|
1250
|
+
let enabled = client.boolVariation("${t}", default: false)
|
|
1251
|
+
if enabled {
|
|
1252
|
+
// new code path
|
|
1253
|
+
}`,
|
|
1254
|
+
notes: X
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
//#endregion
|
|
1259
|
+
//#region src/tools/workflows/wrap-feature.ts
|
|
1260
|
+
function ie(e, t) {
|
|
1261
|
+
e.registerTool("wrap_feature", {
|
|
1262
|
+
title: "Wrap a feature in a new flag",
|
|
1263
|
+
description: "Create a Boolean feature flag and get back the SDK code snippet to guard the new code path with it. Returns the snippet only — apply the edit yourself. The flag starts DISABLED in every environment; enable it with toggle_flag when ready.",
|
|
1264
|
+
inputSchema: n.object({
|
|
1265
|
+
project: n.string().describe("Project key"),
|
|
1266
|
+
key: n.string().describe("Flag key, e.g. new-checkout"),
|
|
1267
|
+
name: n.string().optional().describe("Display name; defaults to the key"),
|
|
1268
|
+
description: n.string().optional().describe("What this flag guards"),
|
|
1269
|
+
tags: n.array(n.string()).optional().describe("Tags for filtering, e.g. team or area"),
|
|
1270
|
+
language: n.enum(K).describe("SDK language of the codebase being edited"),
|
|
1271
|
+
idempotency_key: n.string().optional().describe("Idempotency-Key header for safe retries")
|
|
1272
|
+
}),
|
|
1273
|
+
outputSchema: S({
|
|
1274
|
+
flag: v("POST /api/v1/orgs/{org}/projects/{project}/flags"),
|
|
1275
|
+
integration: w({
|
|
1276
|
+
language: { type: "string" },
|
|
1277
|
+
install: { type: "string" },
|
|
1278
|
+
code: { type: "string" },
|
|
1279
|
+
notes: { type: "string" }
|
|
1280
|
+
}),
|
|
1281
|
+
next_steps: {
|
|
1282
|
+
type: "array",
|
|
1283
|
+
items: { type: "string" }
|
|
1284
|
+
}
|
|
1285
|
+
}, [
|
|
1286
|
+
"flag",
|
|
1287
|
+
"integration",
|
|
1288
|
+
"next_steps"
|
|
1289
|
+
]),
|
|
1290
|
+
annotations: { destructiveHint: !1 }
|
|
1291
|
+
}, async ({ project: e, key: n, name: r, description: i, tags: o, language: s, idempotency_key: c }) => f(async () => {
|
|
1292
|
+
let l = await t.api.request("POST", `/api/v1/orgs/${a(t.org)}/projects/${a(e)}/flags`, {
|
|
1293
|
+
body: {
|
|
1294
|
+
key: n,
|
|
1295
|
+
name: r ?? n,
|
|
1296
|
+
type: "Boolean",
|
|
1297
|
+
description: i,
|
|
1298
|
+
tags: o,
|
|
1299
|
+
...J(s) ? { clientSideVisible: !0 } : {}
|
|
1300
|
+
},
|
|
1301
|
+
idempotencyKey: c
|
|
1302
|
+
}), d = re(s, n);
|
|
1303
|
+
return u({
|
|
1304
|
+
flag: l,
|
|
1305
|
+
integration: {
|
|
1306
|
+
language: s,
|
|
1307
|
+
install: d.install,
|
|
1308
|
+
code: d.code,
|
|
1309
|
+
notes: d.notes
|
|
1310
|
+
},
|
|
1311
|
+
next_steps: [
|
|
1312
|
+
"Apply the snippet around the new code path.",
|
|
1313
|
+
`Enable in a non-production environment first: toggle_flag { project: "${e}", flag: "${n}", environment: "<env>", enabled: true }.`,
|
|
1314
|
+
"Ramp with update_targeting (rolloutPercentage) when ready."
|
|
1315
|
+
]
|
|
1316
|
+
});
|
|
1317
|
+
}));
|
|
1318
|
+
}
|
|
1319
|
+
//#endregion
|
|
1320
|
+
//#region src/server.ts
|
|
1321
|
+
function Z(t) {
|
|
1322
|
+
let n = new e({
|
|
1323
|
+
name: "featureflip",
|
|
1324
|
+
version: r
|
|
1325
|
+
});
|
|
1326
|
+
return k(n, t), A(n, t), j(n, t), F(n, t), te(n, t), R(n, t), z(n, t), H(n, t), G(n, t), ie(n, t), n;
|
|
1327
|
+
}
|
|
1328
|
+
//#endregion
|
|
1329
|
+
//#region src/config.ts
|
|
1330
|
+
var Q = "FEATUREFLIP_TOKEN is required. Create one in the Featureflip dashboard — Settings → API Tokens (personal token, ffp_...) or Organization Settings → Service Tokens (ffs_...) — and set it in your MCP client config.";
|
|
1331
|
+
function $(e = process.env) {
|
|
1332
|
+
let t = e.FEATUREFLIP_TOKEN?.trim();
|
|
1333
|
+
return t ? {
|
|
1334
|
+
token: t,
|
|
1335
|
+
baseUrl: (e.FEATUREFLIP_API_URL?.trim() || "https://api.featureflip.io").replace(/\/+$/, ""),
|
|
1336
|
+
org: e.FEATUREFLIP_ORG?.trim() || void 0
|
|
1337
|
+
} : null;
|
|
1338
|
+
}
|
|
1339
|
+
function ae(e = process.env) {
|
|
1340
|
+
let t = $(e);
|
|
1341
|
+
if (!t) throw Error(Q);
|
|
1342
|
+
return t;
|
|
1343
|
+
}
|
|
1344
|
+
//#endregion
|
|
1345
|
+
//#region src/org.ts
|
|
1346
|
+
async function oe(e, t) {
|
|
1347
|
+
if (t) return t;
|
|
1348
|
+
let n = (await e.request("GET", "/api/v1/orgs", { query: { limit: 100 } })).items ?? [];
|
|
1349
|
+
if (n.length === 1) {
|
|
1350
|
+
let e = n[0].slug;
|
|
1351
|
+
if (!e) throw Error("The token can see one organization, but it has no slug set.");
|
|
1352
|
+
return e;
|
|
1353
|
+
}
|
|
1354
|
+
throw n.length === 0 ? Error("The token can see no organizations. Check the token in the Featureflip dashboard.") : Error(`The token can see ${n.length} organizations — set FEATUREFLIP_ORG to one of: ` + n.map((e) => e.slug ?? "?").join(", "));
|
|
1355
|
+
}
|
|
1356
|
+
//#endregion
|
|
1357
|
+
export { Z as a, r as c, $ as i, Q as n, i as o, ae as r, l as s, oe as t };
|