@featureflip/mcp 0.1.12 → 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.
@@ -1,1034 +0,0 @@
1
- import { McpServer as e } from "@modelcontextprotocol/server";
2
- import * as t from "zod/v4";
3
- //#endregion
4
- //#region src/version.ts
5
- var n = {
6
- name: "@featureflip/mcp",
7
- version: "0.1.12",
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",
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.3",
56
- "vite-plugin-dts": "^5.1.2",
57
- vitest: "^5.0.3"
58
- },
59
- engines: { node: ">=20.19.0" }
60
- }.version, r = 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 i(e) {
68
- return encodeURIComponent(e);
69
- }
70
- var a = /* @__PURE__ */ new Set([
71
- 502,
72
- 503,
73
- 504
74
- ]), o = /* @__PURE__ */ new Set([
75
- "GET",
76
- "HEAD",
77
- "PUT",
78
- "DELETE"
79
- ]), s = [250, 1e3], c = 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 i = new URL(this.opts.baseUrl + t);
88
- for (let [e, t] of Object.entries(n.query ?? {})) t !== void 0 && i.searchParams.set(e, String(t));
89
- let c = {
90
- Authorization: `Bearer ${this.opts.token}`,
91
- Accept: "application/json"
92
- }, l;
93
- n.body !== void 0 && (c["Content-Type"] = "application/json", l = JSON.stringify(n.body)), n.idempotencyKey && (c["Idempotency-Key"] = n.idempotencyKey);
94
- let u = o.has(e) || n.idempotencyKey !== void 0, d;
95
- for (let t = 0;; t++) {
96
- let n = t < s.length;
97
- try {
98
- d = await this.fetchImpl(i.toString(), {
99
- method: e,
100
- headers: c,
101
- body: l
102
- });
103
- } catch (e) {
104
- if (!u || !n) throw e;
105
- await this.sleep(s[t]);
106
- continue;
107
- }
108
- if (!(u ? a.has(d.status) : d.status === 503) || !n) break;
109
- await d.body?.cancel(), await this.sleep(s[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 ${i.pathname}`
120
- };
121
- }
122
- throw new r(d.status, e);
123
- }
124
- return await d.json();
125
- }
126
- }
127
- };
128
- //#endregion
129
- //#region src/errors.ts
130
- function l(e) {
131
- return { content: [{
132
- type: "text",
133
- text: JSON.stringify(e, null, 2)
134
- }] };
135
- }
136
- function u(e) {
137
- if (e instanceof r) {
138
- let t = e.envelope, n = [`Featureflip API error ${e.status} (${t.error}): ${t.message}`];
139
- 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}`), {
140
- content: [{
141
- type: "text",
142
- text: n.join("\n")
143
- }],
144
- isError: !0
145
- };
146
- }
147
- return {
148
- content: [{
149
- type: "text",
150
- text: `Error: ${e instanceof Error ? e.message : String(e)}`
151
- }],
152
- isError: !0
153
- };
154
- }
155
- async function d(e) {
156
- try {
157
- return await e();
158
- } catch (e) {
159
- return u(e);
160
- }
161
- }
162
- //#endregion
163
- //#region src/tools/projects.ts
164
- var f = {
165
- limit: t.number().int().min(1).max(100).optional().describe("Page size (default 20)"),
166
- cursor: t.string().optional().describe("next_cursor from a previous response")
167
- };
168
- function p(e, n) {
169
- e.registerTool("list_projects", {
170
- title: "List projects",
171
- description: "List projects in the Featureflip organization. Paginated: pass cursor from next_cursor to continue.",
172
- inputSchema: t.object({ ...f }),
173
- annotations: { readOnlyHint: !0 }
174
- }, async ({ limit: e, cursor: t }) => d(async () => l(await n.api.request("GET", `/api/v1/orgs/${i(n.org)}/projects`, { query: {
175
- limit: e,
176
- cursor: t
177
- } }))));
178
- }
179
- //#endregion
180
- //#region src/tools/environments.ts
181
- function m(e, n) {
182
- e.registerTool("list_environments", {
183
- title: "List environments",
184
- description: "List the environments of a project (e.g. development, staging, production).",
185
- inputSchema: t.object({
186
- project: t.string().describe("Project key"),
187
- limit: t.number().int().min(1).max(100).optional(),
188
- cursor: t.string().optional()
189
- }),
190
- annotations: { readOnlyHint: !0 }
191
- }, async ({ project: e, limit: t, cursor: r }) => d(async () => l(await n.api.request("GET", `/api/v1/orgs/${i(n.org)}/projects/${i(e)}/environments`, { query: {
192
- limit: t,
193
- cursor: r
194
- } }))));
195
- }
196
- //#endregion
197
- //#region src/tools/segments.ts
198
- function h(e, n) {
199
- e.registerTool("list_segments", {
200
- title: "List user segments",
201
- description: "List reusable user segments in a project. Segments are referenced from targeting rules via userSegmentId.",
202
- inputSchema: t.object({
203
- project: t.string().describe("Project key"),
204
- limit: t.number().int().min(1).max(100).optional(),
205
- cursor: t.string().optional()
206
- }),
207
- annotations: { readOnlyHint: !0 }
208
- }, async ({ project: e, limit: t, cursor: r }) => d(async () => l(await n.api.request("GET", `/api/v1/orgs/${i(n.org)}/projects/${i(e)}/segments`, { query: {
209
- limit: t,
210
- cursor: r
211
- } })))), e.registerTool("get_segment", {
212
- title: "Get user segment",
213
- description: "Get one user segment (its conditions, id, and metadata) by key or id.",
214
- inputSchema: t.object({
215
- project: t.string().describe("Project key"),
216
- segment: t.string().describe("Segment key or id")
217
- }),
218
- annotations: { readOnlyHint: !0 }
219
- }, async ({ project: e, segment: t }) => d(async () => l(await n.api.request("GET", `/api/v1/orgs/${i(n.org)}/projects/${i(e)}/segments/${i(t)}`))));
220
- }
221
- //#endregion
222
- //#region src/tools/owner.ts
223
- var g = t.string().optional().describe("Only flags with this owner: an email, \"me\" for the token's own user, or \"none\" for unowned flags");
224
- async function _(e, t) {
225
- let n = t?.trim();
226
- if (!n) return;
227
- if (n.toLowerCase() !== "me") return n;
228
- let r = await e.api.request("GET", "/api/v1/me");
229
- 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.");
230
- return r.email;
231
- }
232
- //#endregion
233
- //#region src/tools/flags.ts
234
- var v = t.enum([
235
- "Boolean",
236
- "String",
237
- "Number",
238
- "Json"
239
- ]);
240
- function y(e, n) {
241
- let r = (e) => `/api/v1/orgs/${i(n.org)}/projects/${i(e)}/flags`;
242
- e.registerTool("list_flags", {
243
- title: "List feature flags",
244
- 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.",
245
- inputSchema: t.object({
246
- project: t.string().describe("Project key"),
247
- search: t.string().optional(),
248
- tag: t.string().optional(),
249
- type: v.optional(),
250
- archived: t.boolean().optional().describe("true = only archived, false = only active"),
251
- owner: g,
252
- limit: t.number().int().min(1).max(100).optional(),
253
- cursor: t.string().optional()
254
- }),
255
- annotations: { readOnlyHint: !0 }
256
- }, async ({ project: e, owner: t, ...i }) => d(async () => l(await n.api.request("GET", r(e), { query: {
257
- ...i,
258
- owner: await _(n, t)
259
- } })))), e.registerTool("get_flag", {
260
- title: "Get feature flag",
261
- description: "Get one feature flag with its variations and metadata, including its owner (null when unowned). Address by flag key or id.",
262
- inputSchema: t.object({
263
- project: t.string().describe("Project key"),
264
- flag: t.string().describe("Flag key or id")
265
- }),
266
- annotations: { readOnlyHint: !0 }
267
- }, async ({ project: e, flag: t }) => d(async () => l(await n.api.request("GET", `${r(e)}/${i(t)}`)))), e.registerTool("create_flag", {
268
- title: "Create feature flag",
269
- 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).",
270
- inputSchema: t.object({
271
- project: t.string().describe("Project key"),
272
- key: t.string().describe("Unique flag key, e.g. checkout-v2"),
273
- name: t.string(),
274
- type: v,
275
- description: t.string().optional(),
276
- tags: t.array(t.string()).optional(),
277
- clientSideVisible: t.boolean().optional().describe("Expose to client-side SDKs (browser/mobile)"),
278
- initialVariations: t.array(t.object({
279
- key: t.string(),
280
- name: t.string().optional(),
281
- value: t.string().describe("Variation value serialized as a string"),
282
- description: t.string().optional()
283
- })).optional(),
284
- expiresAtUtc: t.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)"),
285
- ownerEmail: t.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"),
286
- isPermanent: t.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"),
287
- idempotency_key: t.string().optional().describe("Idempotency-Key header for safe retries")
288
- }),
289
- annotations: { destructiveHint: !1 }
290
- }, async ({ project: e, idempotency_key: t, ...i }) => d(async () => l(await n.api.request("POST", r(e), {
291
- body: i,
292
- idempotencyKey: t
293
- })))), e.registerTool("update_flag", {
294
- title: "Update feature flag metadata",
295
- 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.",
296
- inputSchema: t.object({
297
- project: t.string(),
298
- flag: t.string().describe("Flag key or id"),
299
- name: t.string().optional(),
300
- description: t.string().optional(),
301
- tags: t.array(t.string()).optional(),
302
- clientSideVisible: t.boolean().optional()
303
- }),
304
- annotations: { destructiveHint: !0 }
305
- }, async ({ project: e, flag: t, name: a, description: o, tags: s, clientSideVisible: c }) => d(async () => {
306
- let u = `${r(e)}/${i(t)}`, d = a === void 0 || o === void 0 ? await n.api.request("GET", u) : void 0, f = {
307
- name: a ?? d?.name,
308
- description: o ?? d?.description,
309
- tags: s,
310
- clientSideVisible: c
311
- };
312
- return await n.api.request("PUT", u, { body: f }), l({
313
- project: e,
314
- flag: t,
315
- updated: !0
316
- });
317
- })), e.registerTool("set_flag_expiry", {
318
- title: "Set or clear flag expiry",
319
- 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.",
320
- inputSchema: t.object({
321
- project: t.string(),
322
- flag: t.string().describe("Flag key or id"),
323
- expiresAtUtc: t.string().nullable().describe("Date (YYYY-MM-DD, end of that UTC day) or ISO-8601 timestamp, or null to clear the expiry")
324
- }),
325
- annotations: {
326
- destructiveHint: !1,
327
- idempotentHint: !0
328
- }
329
- }, async ({ project: e, flag: t, expiresAtUtc: a }) => d(async () => {
330
- let o = `${r(e)}/${i(t)}/expiry`;
331
- return a === null ? await n.api.request("DELETE", o) : await n.api.request("PUT", o, { body: { expiresAtUtc: a } }), l({
332
- project: e,
333
- flag: t,
334
- expiresAtUtc: a
335
- });
336
- })), e.registerTool("set_flag_permanent", {
337
- title: "Mark a flag permanent or temporary",
338
- 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.",
339
- inputSchema: t.object({
340
- project: t.string(),
341
- flag: t.string().describe("Flag key or id"),
342
- permanent: t.boolean().describe("true to mark the flag permanent, false to make it temporary")
343
- }),
344
- annotations: {
345
- destructiveHint: !1,
346
- idempotentHint: !0
347
- }
348
- }, async ({ project: e, flag: t, permanent: a }) => d(async () => {
349
- let o = `${r(e)}/${i(t)}/permanent`;
350
- return await n.api.request(a ? "PUT" : "DELETE", o), l({
351
- project: e,
352
- flag: t,
353
- permanent: a
354
- });
355
- })), e.registerTool("set_flag_owner", {
356
- title: "Set or clear flag owner",
357
- 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.",
358
- inputSchema: t.object({
359
- project: t.string(),
360
- flag: t.string().describe("Flag key or id"),
361
- owner: t.string().nullable().describe("Owner email, \"me\" for the token's own user, or null (or \"none\") to clear the owner")
362
- }),
363
- annotations: {
364
- destructiveHint: !1,
365
- idempotentHint: !0
366
- }
367
- }, async ({ project: e, flag: t, owner: a }) => d(async () => {
368
- let o = `${r(e)}/${i(t)}/owner`, s = a?.trim().toLowerCase() === "none" ? void 0 : await _(n, a ?? void 0);
369
- return s === void 0 ? await n.api.request("DELETE", o) : await n.api.request("PUT", o, { body: { email: s } }), l({
370
- project: e,
371
- flag: t,
372
- owner: s ?? null
373
- });
374
- })), e.registerTool("delete_flag", {
375
- title: "Delete feature flag",
376
- 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.",
377
- inputSchema: t.object({
378
- project: t.string(),
379
- flag: t.string().describe("Flag key or id")
380
- }),
381
- annotations: { destructiveHint: !0 }
382
- }, async ({ project: e, flag: t }) => d(async () => (await n.api.request("DELETE", `${r(e)}/${i(t)}`), l({ deleted: t })))), e.registerTool("archive_flag", {
383
- title: "Archive feature flag",
384
- 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.",
385
- inputSchema: t.object({
386
- project: t.string(),
387
- flag: t.string(),
388
- force: t.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.")
389
- }),
390
- annotations: { destructiveHint: !0 }
391
- }, async ({ project: e, flag: t, force: a }) => d(async () => (await n.api.request("POST", `${r(e)}/${i(t)}/archive`, { query: a ? { force: !0 } : {} }), l({
392
- flag: t,
393
- archived: !0
394
- })))), e.registerTool("restore_flag", {
395
- title: "Restore archived flag",
396
- description: "Restore a previously archived flag.",
397
- inputSchema: t.object({
398
- project: t.string(),
399
- flag: t.string()
400
- }),
401
- annotations: { destructiveHint: !1 }
402
- }, async ({ project: e, flag: t }) => d(async () => (await n.api.request("POST", `${r(e)}/${i(t)}/restore`), l({
403
- flag: t,
404
- archived: !1
405
- }))));
406
- }
407
- //#endregion
408
- //#region src/tools/flag-environments.ts
409
- var b = t.enum([
410
- "SingleVariation",
411
- "PercentageRollout",
412
- "TargetedRollout"
413
- ]);
414
- function x(e, n) {
415
- let r = (e, t) => `/api/v1/orgs/${i(n.org)}/projects/${i(e)}/flags/${i(t)}`;
416
- e.registerTool("toggle_flag", {
417
- title: "Toggle flag in environment",
418
- description: "Enable or disable a flag in ONE environment. Affects live evaluation immediately — double-check the environment.",
419
- inputSchema: t.object({
420
- project: t.string(),
421
- flag: t.string().describe("Flag key or id"),
422
- environment: t.string().describe("Environment key, e.g. production"),
423
- enabled: t.boolean()
424
- }),
425
- annotations: { destructiveHint: !0 }
426
- }, async ({ project: e, flag: t, environment: a, enabled: o }) => d(async () => (await n.api.request("POST", `${r(e, t)}/environments/${i(a)}/toggle`, { body: { enabled: o } }), l({
427
- project: e,
428
- flag: t,
429
- environment: a,
430
- enabled: o
431
- })))), e.registerTool("update_flag_environment_config", {
432
- title: "Update flag environment config",
433
- 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.",
434
- inputSchema: t.object({
435
- project: t.string(),
436
- flag: t.string(),
437
- environment: t.string(),
438
- defaultVariationId: t.string().describe("Variation id to serve on fallthrough — required by the API"),
439
- strategy: b.optional(),
440
- prerequisites: t.array(t.object({
441
- prerequisiteFlagKey: t.string(),
442
- expectedVariationKey: t.string()
443
- })).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.")
444
- }),
445
- annotations: { destructiveHint: !0 }
446
- }, async ({ project: e, flag: t, environment: a, ...o }) => d(async () => (await n.api.request("PUT", `${r(e, t)}/environments/${i(a)}`, { body: o }), l({
447
- project: e,
448
- flag: t,
449
- environment: a,
450
- ...o
451
- })))), e.registerTool("flag_status", {
452
- title: "Flag status across environments",
453
- description: "Compact cross-environment view of one flag: enabled state, strategy, default variation, and prerequisites per environment.",
454
- inputSchema: t.object({
455
- project: t.string(),
456
- flag: t.string()
457
- }),
458
- annotations: { readOnlyHint: !0 }
459
- }, async ({ project: e, flag: t }) => d(async () => {
460
- let [i, a] = await Promise.all([n.api.request("GET", r(e, t)), n.api.request("GET", `${r(e, t)}/environments`)]), o = (e) => (i.variations ?? []).find((t) => t.id === e)?.key ?? e ?? null;
461
- return l({
462
- key: i.key ?? null,
463
- name: i.name ?? null,
464
- type: i.type ?? null,
465
- archived: i.isArchived ?? !1,
466
- updatedAt: i.updatedAt ?? null,
467
- environments: a.map((e) => ({
468
- environment: e.environmentKey ?? null,
469
- enabled: e.isEnabled ?? !1,
470
- strategy: e.strategy ?? null,
471
- defaultVariation: o(e.defaultVariationId),
472
- prerequisites: e.prerequisites ?? []
473
- }))
474
- });
475
- }));
476
- }
477
- //#endregion
478
- //#region src/tools/targeting.ts
479
- var S = t.object({
480
- attribute: t.string().describe("Context attribute name, e.g. plan, country, user_id"),
481
- operator: t.string().describe("OperatorType member name, case-sensitive — e.g. Equals, NotEquals, Contains, In, GreaterThan"),
482
- values: t.array(t.string()),
483
- negate: t.boolean().optional().default(!1)
484
- }), C = t.object({
485
- description: t.string().optional(),
486
- variationId: t.string().optional().describe("Variation served when the rule matches"),
487
- rolloutPercentage: t.number().int().min(0).max(100).optional().describe("Percentage rollout for this rule"),
488
- userSegmentId: t.string().optional().describe("Match users in this segment (see list_segments)"),
489
- conditionGroups: t.array(t.object({
490
- operator: t.enum(["And", "Or"]).describe("How conditions inside this group combine"),
491
- conditions: t.array(S)
492
- })).optional().describe("Groups are ANDed together; conditions inside a group use the group operator")
493
- });
494
- function w(e, n) {
495
- let r = (e, t, r) => `/api/v1/orgs/${i(n.org)}/projects/${i(e)}/flags/${i(t)}/environments/${i(r)}/targeting`;
496
- e.registerTool("get_targeting", {
497
- title: "Get targeting rules",
498
- description: "Get a flag's current targeting configuration (enabled flag + ordered rules) in one environment.",
499
- inputSchema: t.object({
500
- project: t.string(),
501
- flag: t.string(),
502
- environment: t.string()
503
- }),
504
- annotations: { readOnlyHint: !0 }
505
- }, async ({ project: e, flag: t, environment: i }) => d(async () => l(await n.api.request("GET", r(e, t, i))))), e.registerTool("update_targeting", {
506
- title: "Replace targeting rules",
507
- 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.",
508
- inputSchema: t.object({
509
- project: t.string(),
510
- flag: t.string(),
511
- environment: t.string(),
512
- rules: t.array(C)
513
- }),
514
- annotations: { destructiveHint: !0 }
515
- }, async ({ project: e, flag: t, environment: i, rules: a }) => d(async () => (await n.api.request("PUT", r(e, t, i), { body: { rules: a } }), l({
516
- project: e,
517
- flag: t,
518
- environment: i,
519
- rulesReplaced: a.length
520
- }))));
521
- }
522
- //#endregion
523
- //#region src/tools/variations.ts
524
- function T(e, n) {
525
- e.registerTool("manage_variation", {
526
- title: "Add, update, or remove a variation",
527
- 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.",
528
- inputSchema: t.object({
529
- project: t.string(),
530
- flag: t.string(),
531
- action: t.enum([
532
- "add",
533
- "update",
534
- "remove"
535
- ]),
536
- variationId: t.string().optional().describe("Required for update/remove"),
537
- key: t.string().optional().describe("Required for add; immutable afterwards"),
538
- name: t.string().optional(),
539
- value: t.string().optional().describe("Variation value serialized as a string; required for add"),
540
- description: t.string().optional()
541
- }),
542
- annotations: { destructiveHint: !0 }
543
- }, async ({ project: e, flag: t, action: r, variationId: a, key: o, name: s, value: c, description: f }) => d(async () => {
544
- let d = `/api/v1/orgs/${i(n.org)}/projects/${i(e)}/flags/${i(t)}/variations`;
545
- return r === "add" ? !o || c === void 0 ? u(/* @__PURE__ */ Error("action=add requires key and value")) : l(await n.api.request("POST", d, { body: {
546
- key: o,
547
- name: s,
548
- value: c,
549
- description: f
550
- } })) : a ? r === "update" ? (await n.api.request("PUT", `${d}/${i(a)}`, { body: {
551
- name: s,
552
- value: c,
553
- description: f
554
- } }), l({ updated: a })) : (await n.api.request("DELETE", `${d}/${i(a)}`), l({ removed: a })) : u(/* @__PURE__ */ Error(`action=${r} requires variationId (see get_flag for ids)`));
555
- }));
556
- }
557
- //#endregion
558
- //#region src/tools/webhooks.ts
559
- var E = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, D = "Requires an Admin token. A 404 on every webhook call means webhooks are not enabled for the organization.", O = {
560
- limit: t.number().int().min(1).max(100).optional().describe("Page size (default 20)"),
561
- cursor: t.string().optional().describe("next_cursor from a previous response")
562
- };
563
- function k(e, n) {
564
- let r = `/api/v1/orgs/${i(n.org)}/webhooks`, a = (e) => `${r}/${i(e)}`;
565
- async function o(e) {
566
- let t = [];
567
- for (let r of e) {
568
- if (E.test(r)) {
569
- t.push(r);
570
- continue;
571
- }
572
- let e = await n.api.request("GET", `/api/v1/orgs/${i(n.org)}/projects/${i(r)}`);
573
- if (!e?.id) throw Error(`Project "${r}" resolved without an id`);
574
- t.push(e.id);
575
- }
576
- return t;
577
- }
578
- async function s(e) {
579
- let t = e.map((e) => {
580
- if (E.test(e)) return { id: e };
581
- let t = e.indexOf("/"), n = e.slice(0, t), r = e.slice(t + 1);
582
- if (t < 0 || !n || !r) throw Error(`Environment "${e}" needs its project: pass "<project>/<environment>", e.g. "web/production", or the environment's id.`);
583
- return {
584
- project: n,
585
- env: r
586
- };
587
- }), r = [];
588
- for (let e of t) {
589
- if ("id" in e) {
590
- r.push(e.id);
591
- continue;
592
- }
593
- let t = await n.api.request("GET", `/api/v1/orgs/${i(n.org)}/projects/${i(e.project)}/environments/${i(e.env)}`);
594
- if (!t?.id) throw Error(`Environment "${e.project}/${e.env}" resolved without an id`);
595
- r.push(t.id);
596
- }
597
- return r;
598
- }
599
- e.registerTool("list_webhooks", {
600
- title: "List webhook subscriptions",
601
- 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. " + D,
602
- inputSchema: t.object({ ...O }),
603
- annotations: { readOnlyHint: !0 }
604
- }, async ({ limit: e, cursor: t }) => d(async () => {
605
- let [i, a] = await Promise.all([n.api.request("GET", r, { query: {
606
- limit: e,
607
- cursor: t
608
- } }), n.api.request("GET", `${r}/event-types`)]);
609
- return l({
610
- ...i,
611
- availableEventTypes: a
612
- });
613
- })), e.registerTool("list_webhook_deliveries", {
614
- title: "List webhook deliveries",
615
- 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. " + D,
616
- inputSchema: t.object({
617
- id: t.string().describe("Subscription id (from list_webhooks)"),
618
- ...O
619
- }),
620
- annotations: { readOnlyHint: !0 }
621
- }, async ({ id: e, limit: t, cursor: r }) => d(async () => l(await n.api.request("GET", `${a(e)}/deliveries`, { query: {
622
- limit: t,
623
- cursor: r
624
- } })))), e.registerTool("manage_webhook", {
625
- title: "Create, update, or delete a webhook subscription, or rotate its secret",
626
- 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. " + D,
627
- inputSchema: t.object({
628
- action: t.enum([
629
- "create",
630
- "update",
631
- "delete",
632
- "rotate_secret",
633
- "retire_secret"
634
- ]),
635
- id: t.string().optional().describe("Subscription id; required for everything except create"),
636
- name: t.string().optional().describe("Required for create"),
637
- url: t.string().optional().describe("Receiver URL; required for create"),
638
- eventTypes: t.array(t.string()).optional().describe("Event types to send; empty or omitted = all"),
639
- projects: t.array(t.string()).optional().describe("Project keys or ids to cover; empty or omitted on create = all"),
640
- environments: t.array(t.string()).optional().describe("\"<project>/<environment>\" keys or environment ids; empty or omitted on create = all"),
641
- enabled: t.boolean().optional().describe("update only; subscriptions are created enabled"),
642
- secretId: t.string().optional().describe("Required for retire_secret"),
643
- idempotency_key: t.string().optional().describe("Idempotency-Key header for create / rotate_secret")
644
- }),
645
- annotations: { destructiveHint: !0 }
646
- }, async ({ action: e, id: t, name: c, url: f, eventTypes: p, projects: m, environments: h, enabled: g, secretId: _, idempotency_key: v }) => d(async () => {
647
- if (e === "create") {
648
- if (!c || !f) return u(/* @__PURE__ */ Error("action=create requires name and url"));
649
- let e = h && await s(h), t = m && await o(m);
650
- return l(await n.api.request("POST", r, {
651
- body: {
652
- name: c,
653
- url: f,
654
- provider: "GenericHttp",
655
- eventTypes: p,
656
- projectIds: t,
657
- environmentIds: e
658
- },
659
- idempotencyKey: v
660
- }));
661
- }
662
- if (!t) return u(/* @__PURE__ */ Error(`action=${e} requires id (see list_webhooks)`));
663
- if (e === "update") {
664
- let e = h && await s(h), r = m && await o(m), i = await n.api.request("GET", a(t)), u = {
665
- name: c ?? i.name,
666
- url: f ?? i.url,
667
- eventTypes: p ?? i.eventTypes,
668
- projectIds: r ?? i.projectIds,
669
- environmentIds: e ?? i.environmentIds,
670
- isEnabled: g ?? i.isEnabled
671
- };
672
- return await n.api.request("PUT", a(t), { body: u }), l({
673
- id: t,
674
- updated: !0,
675
- ...u
676
- });
677
- }
678
- return e === "delete" ? (await n.api.request("DELETE", a(t)), l({ deleted: t })) : e === "rotate_secret" ? l(await n.api.request("POST", `${a(t)}/secrets`, { idempotencyKey: v })) : _ ? (await n.api.request("DELETE", `${a(t)}/secrets/${i(_)}`), l({
679
- id: t,
680
- retiredSecretId: _
681
- })) : u(/* @__PURE__ */ Error("action=retire_secret requires secretId (see list_webhooks)"));
682
- })), e.registerTool("deliver_webhook", {
683
- title: "Send a test event or redeliver",
684
- 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. " + D,
685
- inputSchema: t.object({
686
- action: t.enum(["test", "redeliver"]),
687
- id: t.string().describe("Subscription id (from list_webhooks)"),
688
- deliveryId: t.string().optional().describe("Required for redeliver")
689
- }),
690
- annotations: {
691
- destructiveHint: !1,
692
- openWorldHint: !0
693
- }
694
- }, async ({ action: e, id: t, deliveryId: r }) => d(async () => e === "test" ? l(await n.api.request("POST", `${a(t)}/test`)) : r ? (await n.api.request("POST", `${a(t)}/deliveries/${i(r)}/redeliver`), l({
695
- id: t,
696
- redelivered: r
697
- })) : u(/* @__PURE__ */ Error("action=redeliver requires deliveryId (see list_webhook_deliveries)"))));
698
- }
699
- //#endregion
700
- //#region src/tools/workflows/stale-flags.ts
701
- var A = t.enum(["dead", "stale"]), j = 50;
702
- function M(e, n) {
703
- e.registerTool("find_stale_flags", {
704
- title: "Find stale flags",
705
- 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. Every result carries expiresAtUtc, 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 ${j} candidates per call, expired flags first.`,
706
- inputSchema: t.object({
707
- project: t.string(),
708
- days: t.number().int().min(1).optional().default(30).describe("Minimum age in days since last update"),
709
- owner: g
710
- }),
711
- annotations: { readOnlyHint: !0 }
712
- }, async ({ project: e, days: t, owner: a }) => d(async () => {
713
- let o = await _(n, a), s = `/api/v1/orgs/${i(n.org)}/projects/${i(e)}/flags`, c = Date.now(), u = c - t * 24 * 60 * 60 * 1e3, d = (e) => !!e.expiresAtUtc && new Date(e.expiresAtUtc).getTime() <= c, f = [], p;
714
- do {
715
- let e = await n.api.request("GET", s, { query: {
716
- archived: !1,
717
- limit: 100,
718
- cursor: p,
719
- owner: o,
720
- permanent: !1
721
- } });
722
- f.push(...(e.items ?? []).filter((e) => !!e.key && !!e.name && !!e.updatedAt && (new Date(e.updatedAt).getTime() < u || d(e)))), p = e.next_cursor ?? void 0;
723
- } while (p);
724
- f.sort((e, t) => Number(d(t)) - Number(d(e)));
725
- let m = f.length > j, h = f.slice(0, j), g = [];
726
- for (let e of h) {
727
- let t = await n.api.request("GET", `${s}/${i(e.key)}/environments`);
728
- if (t.length === 0) continue;
729
- let r = d(e), a = t.every((e) => e.isEnabled), o = t.every((e) => !e.isEnabled);
730
- (a || o || r) && g.push({
731
- key: e.key,
732
- name: e.name,
733
- updatedAt: e.updatedAt,
734
- expiresAtUtc: e.expiresAtUtc ?? null,
735
- expired: r,
736
- owner: e.owner ?? null,
737
- reason: a ? "enabled-everywhere" : o ? "disabled-everywhere" : "past-expiry",
738
- environments: t.length,
739
- blockedBy: null
740
- });
741
- }
742
- let v;
743
- if (g.length > 0) try {
744
- let e = /* @__PURE__ */ new Map(), t;
745
- do {
746
- let r = await n.api.request("GET", `${s}/removal-candidates`, { query: {
747
- staleness: "stale",
748
- limit: 100,
749
- cursor: t
750
- } });
751
- for (let t of r.items ?? []) t.key && t.blockedBy && e.set(t.key, t.blockedBy);
752
- t = r.next_cursor ?? void 0;
753
- } while (t);
754
- for (let t of g) t.blockedBy = e.get(t.key) ?? null;
755
- } catch (e) {
756
- if (!(e instanceof r)) throw e;
757
- v = `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).`;
758
- }
759
- return l({
760
- project: e,
761
- olderThanDays: t,
762
- ...o ? { owner: o } : {},
763
- checked: h.length,
764
- truncated: m,
765
- ...m ? { note: `Only the first ${j} of ${f.length} candidates were checked.` } : {},
766
- ...v ? { blockedByNote: v } : {},
767
- stale: g
768
- });
769
- })), e.registerTool("list_removal_candidates", {
770
- title: "List removal candidates",
771
- 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.",
772
- inputSchema: t.object({
773
- project: t.string().describe("Project key"),
774
- staleness: A.optional().describe("Minimum staleness tier: dead (default) or stale"),
775
- limit: t.number().int().min(1).max(100).optional(),
776
- cursor: t.string().optional()
777
- }),
778
- annotations: { readOnlyHint: !0 }
779
- }, async ({ project: e, ...t }) => d(async () => l(await n.api.request("GET", `/api/v1/orgs/${i(n.org)}/projects/${i(e)}/flags/removal-candidates`, { query: t }))));
780
- }
781
- //#endregion
782
- //#region src/tools/workflows/snippets.ts
783
- var N = [
784
- "js",
785
- "node",
786
- "browser",
787
- "react",
788
- "python",
789
- "csharp",
790
- "java",
791
- "go",
792
- "php",
793
- "ruby",
794
- "swift"
795
- ], P = [
796
- "browser",
797
- "react",
798
- "swift"
799
- ];
800
- function F(e) {
801
- return P.includes(e);
802
- }
803
- var I = "Uses a SERVER-side SDK key (Featureflip dashboard → environment → SDK keys).", L = "Uses a CLIENT-side SDK key (safe to ship to browsers/apps).";
804
- function R(e, t) {
805
- switch (e) {
806
- case "js": return {
807
- install: "npm install @featureflip/js",
808
- code: `import { FeatureflipClient, createNodePlatform } from '@featureflip/js';
809
-
810
- const client = FeatureflipClient.get(
811
- { sdkKey: process.env.FEATUREFLIP_SDK_KEY!, baseUrl: 'https://eval.featureflip.io' },
812
- createNodePlatform(),
813
- );
814
- await client.waitForInitialization();
815
- const enabled = client.boolVariation('${t}', { user_id: userId }, false);
816
- if (enabled) {
817
- // new code path
818
- }`,
819
- notes: I
820
- };
821
- case "node": return {
822
- install: "npm install @featureflip/node",
823
- code: `import { FeatureflipClient } from '@featureflip/node';
824
-
825
- const client = await FeatureflipClient.create({ sdkKey: process.env.FEATUREFLIP_SDK_KEY! });
826
- const enabled = client.boolVariation('${t}', { user_id: userId }, false);
827
- if (enabled) {
828
- // new code path
829
- }`,
830
- notes: I
831
- };
832
- case "browser": return {
833
- install: "npm install @featureflip/browser",
834
- code: `import { FeatureflipClient } from '@featureflip/browser';
835
-
836
- const client = FeatureflipClient.get({ clientKey: 'YOUR_CLIENT_SDK_KEY' });
837
- await client.initialize();
838
- const enabled = client.boolVariation('${t}', false);
839
- if (enabled) {
840
- // new code path
841
- }`,
842
- notes: L
843
- };
844
- case "react": return {
845
- install: "npm install @featureflip/react @featureflip/browser",
846
- code: `import { FeatureflipProvider, useFeatureFlag } from '@featureflip/react';
847
-
848
- // In your app root:
849
- // <FeatureflipProvider clientKey="YOUR_CLIENT_SDK_KEY">...</FeatureflipProvider>
850
-
851
- function MyComponent() {
852
- const enabled = useFeatureFlag('${t}', false);
853
- return enabled ? <NewExperience /> : <CurrentExperience />;
854
- }`,
855
- notes: L
856
- };
857
- case "python": return {
858
- install: "pip install featureflip",
859
- code: `from featureflip import FeatureflipClient
860
-
861
- client = FeatureflipClient(sdk_key=os.environ["FEATUREFLIP_SDK_KEY"])
862
- enabled = client.variation("${t}", {"user_id": user_id}, default=False)
863
- if enabled:
864
- # new code path
865
- ...`,
866
- notes: I
867
- };
868
- case "csharp": return {
869
- install: "dotnet add package Featureflip.Client",
870
- code: `using Featureflip.Client;
871
-
872
- var client = FeatureflipClient.Get(Environment.GetEnvironmentVariable("FEATUREFLIP_SDK_KEY")!);
873
- var enabled = client.BoolVariation("${t}",
874
- new EvaluationContext { UserId = userId }, false);
875
- if (enabled)
876
- {
877
- // new code path
878
- }`,
879
- notes: I
880
- };
881
- case "java": return {
882
- install: "implementation 'io.featureflip:featureflip-java:2.0.0'",
883
- code: `import io.featureflip.client.FeatureflipClient;
884
- import io.featureflip.client.EvaluationContext;
885
-
886
- FeatureflipClient client = FeatureflipClient.get(System.getenv("FEATUREFLIP_SDK_KEY"));
887
- client.waitForInitialization();
888
- boolean enabled = client.boolVariation("${t}",
889
- EvaluationContext.of(userId), false);
890
- if (enabled) {
891
- // new code path
892
- }`,
893
- notes: I
894
- };
895
- case "go": return {
896
- install: "go get github.com/canopy-labs/featureflip-go/v2",
897
- code: `import featureflip "github.com/canopy-labs/featureflip-go/v2"
898
-
899
- client, err := featureflip.Get(os.Getenv("FEATUREFLIP_SDK_KEY"))
900
- if err != nil {
901
- log.Fatal(err)
902
- }
903
- defer client.Close()
904
-
905
- ctx := featureflip.EvaluationContext{UserID: userID}
906
- if client.BoolVariation("${t}", ctx, false) {
907
- // new code path
908
- }`,
909
- notes: I
910
- };
911
- case "php": return {
912
- install: "composer require featureflip/featureflip-php",
913
- code: `use Featureflip\\FeatureflipClient;
914
- use Featureflip\\Config;
915
-
916
- $client = FeatureflipClient::get(getenv('FEATUREFLIP_SDK_KEY'), new Config(
917
- cache: $psrCache, httpClient: $psrHttpClient,
918
- requestFactory: $psrRequestFactory, streamFactory: $psrStreamFactory,
919
- ));
920
- $enabled = $client->boolVariation('${t}', ['user_id' => $userId], false);
921
- if ($enabled) {
922
- // new code path
923
- }`,
924
- notes: I + " Requires PSR-16 cache and PSR-18 HTTP implementations."
925
- };
926
- case "ruby": return {
927
- install: "gem \"featureflip\"",
928
- code: `require "featureflip"
929
-
930
- client = Featureflip::Client.get(ENV.fetch("FEATUREFLIP_SDK_KEY"))
931
- enabled = client.bool_variation("${t}", { "user_id" => user_id }, false)
932
- if enabled
933
- # new code path
934
- end`,
935
- notes: I
936
- };
937
- case "swift": return {
938
- install: ".package(url: \"https://github.com/canopy-labs/featureflip-swift\", from: \"2.0.0\")",
939
- code: `import Featureflip
940
-
941
- let config = FeatureflipConfig(clientKey: "YOUR_CLIENT_SDK_KEY")
942
- let client = FeatureflipClient(config: config)
943
- await client.initialize()
944
- let enabled = client.boolVariation("${t}", default: false)
945
- if enabled {
946
- // new code path
947
- }`,
948
- notes: L
949
- };
950
- }
951
- }
952
- //#endregion
953
- //#region src/tools/workflows/wrap-feature.ts
954
- function z(e, n) {
955
- e.registerTool("wrap_feature", {
956
- title: "Wrap a feature in a new flag",
957
- 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.",
958
- inputSchema: t.object({
959
- project: t.string().describe("Project key"),
960
- key: t.string().describe("Flag key, e.g. new-checkout"),
961
- name: t.string().optional().describe("Display name; defaults to the key"),
962
- description: t.string().optional().describe("What this flag guards"),
963
- tags: t.array(t.string()).optional(),
964
- language: t.enum(N).describe("SDK language of the codebase being edited"),
965
- idempotency_key: t.string().optional()
966
- }),
967
- annotations: { destructiveHint: !1 }
968
- }, async ({ project: e, key: t, name: r, description: a, tags: o, language: s, idempotency_key: c }) => d(async () => {
969
- let u = await n.api.request("POST", `/api/v1/orgs/${i(n.org)}/projects/${i(e)}/flags`, {
970
- body: {
971
- key: t,
972
- name: r ?? t,
973
- type: "Boolean",
974
- description: a,
975
- tags: o,
976
- ...F(s) ? { clientSideVisible: !0 } : {}
977
- },
978
- idempotencyKey: c
979
- }), d = R(s, t);
980
- return l({
981
- flag: u,
982
- integration: {
983
- language: s,
984
- install: d.install,
985
- code: d.code,
986
- notes: d.notes
987
- },
988
- next_steps: [
989
- "Apply the snippet around the new code path.",
990
- `Enable in a non-production environment first: toggle_flag { project: "${e}", flag: "${t}", environment: "<env>", enabled: true }.`,
991
- "Ramp with update_targeting (rolloutPercentage) when ready."
992
- ]
993
- });
994
- }));
995
- }
996
- //#endregion
997
- //#region src/server.ts
998
- function B(t) {
999
- let r = new e({
1000
- name: "featureflip",
1001
- version: n
1002
- });
1003
- return p(r, t), m(r, t), h(r, t), y(r, t), x(r, t), w(r, t), T(r, t), k(r, t), M(r, t), z(r, t), r;
1004
- }
1005
- //#endregion
1006
- //#region src/config.ts
1007
- var V = "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.";
1008
- function H(e = process.env) {
1009
- let t = e.FEATUREFLIP_TOKEN?.trim();
1010
- return t ? {
1011
- token: t,
1012
- baseUrl: (e.FEATUREFLIP_API_URL?.trim() || "https://api.featureflip.io").replace(/\/+$/, ""),
1013
- org: e.FEATUREFLIP_ORG?.trim() || void 0
1014
- } : null;
1015
- }
1016
- function U(e = process.env) {
1017
- let t = H(e);
1018
- if (!t) throw Error(V);
1019
- return t;
1020
- }
1021
- //#endregion
1022
- //#region src/org.ts
1023
- async function W(e, t) {
1024
- if (t) return t;
1025
- let n = (await e.request("GET", "/api/v1/orgs", { query: { limit: 100 } })).items ?? [];
1026
- if (n.length === 1) {
1027
- let e = n[0].slug;
1028
- if (!e) throw Error("The token can see one organization, but it has no slug set.");
1029
- return e;
1030
- }
1031
- 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(", "));
1032
- }
1033
- //#endregion
1034
- export { B as a, n as c, H as i, V as n, r as o, U as r, c as s, W as t };