@featureflip/mcp 0.1.6 → 0.1.8
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/README.md +3 -1
- package/dist/cli.mjs +1 -1
- package/dist/client.d.ts +2 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/generated/api-types.d.ts +2236 -524
- package/dist/generated/api-types.d.ts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{org-BixGfyQ6.js → org-DZ1ge1Mx.js} +333 -133
- package/dist/server.d.ts.map +1 -1
- package/dist/tools/flags.d.ts.map +1 -1
- package/dist/tools/webhooks.d.ts +4 -0
- package/dist/tools/webhooks.d.ts.map +1 -0
- package/dist/tools/workflows/stale-flags.d.ts.map +1 -1
- package/package.json +17 -5
|
@@ -4,9 +4,21 @@ import * as t from "zod/v4";
|
|
|
4
4
|
//#region src/version.ts
|
|
5
5
|
var n = {
|
|
6
6
|
name: "@featureflip/mcp",
|
|
7
|
-
version: "0.1.
|
|
7
|
+
version: "0.1.8",
|
|
8
8
|
mcpName: "io.github.canopy-labs/featureflip",
|
|
9
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
|
+
],
|
|
10
22
|
license: "Apache-2.0",
|
|
11
23
|
repository: {
|
|
12
24
|
type: "git",
|
|
@@ -32,16 +44,16 @@ var n = {
|
|
|
32
44
|
},
|
|
33
45
|
dependencies: {
|
|
34
46
|
"@modelcontextprotocol/server": "2.0.0",
|
|
35
|
-
zod: "^4.6.
|
|
47
|
+
zod: "^4.6.5"
|
|
36
48
|
},
|
|
37
49
|
devDependencies: {
|
|
38
50
|
"@modelcontextprotocol/client": "2.0.0",
|
|
39
51
|
"@typescript/typescript6": "^6.0.2",
|
|
40
|
-
"@types/node": "^26.
|
|
52
|
+
"@types/node": "^26.6.2",
|
|
41
53
|
typescript: "^7.0.2",
|
|
42
54
|
vite: "^8.3.0",
|
|
43
|
-
"vite-plugin-dts": "^5.1.
|
|
44
|
-
vitest: "^5.0.
|
|
55
|
+
"vite-plugin-dts": "^5.1.1",
|
|
56
|
+
vitest: "^5.0.1"
|
|
45
57
|
},
|
|
46
58
|
engines: { node: ">=20.19.0" }
|
|
47
59
|
}.version, r = class extends Error {
|
|
@@ -54,51 +66,73 @@ var n = {
|
|
|
54
66
|
function i(e) {
|
|
55
67
|
return encodeURIComponent(e);
|
|
56
68
|
}
|
|
57
|
-
var a =
|
|
69
|
+
var a = /* @__PURE__ */ new Set([
|
|
70
|
+
502,
|
|
71
|
+
503,
|
|
72
|
+
504
|
|
73
|
+
]), o = /* @__PURE__ */ new Set([
|
|
74
|
+
"GET",
|
|
75
|
+
"HEAD",
|
|
76
|
+
"PUT",
|
|
77
|
+
"DELETE"
|
|
78
|
+
]), s = [250, 1e3], c = class {
|
|
58
79
|
opts;
|
|
59
80
|
fetchImpl;
|
|
81
|
+
sleep;
|
|
60
82
|
constructor(e) {
|
|
61
|
-
this.opts = e, this.fetchImpl = e.fetchImpl ?? fetch;
|
|
83
|
+
this.opts = e, this.fetchImpl = e.fetchImpl ?? fetch, this.sleep = e.sleep ?? ((e) => new Promise((t) => setTimeout(t, e)));
|
|
62
84
|
}
|
|
63
85
|
async request(e, t, n = {}) {
|
|
64
86
|
let i = new URL(this.opts.baseUrl + t);
|
|
65
87
|
for (let [e, t] of Object.entries(n.query ?? {})) t !== void 0 && i.searchParams.set(e, String(t));
|
|
66
|
-
let
|
|
88
|
+
let c = {
|
|
67
89
|
Authorization: `Bearer ${this.opts.token}`,
|
|
68
90
|
Accept: "application/json"
|
|
69
|
-
},
|
|
70
|
-
n.body !== void 0 && (
|
|
71
|
-
let
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
91
|
+
}, l;
|
|
92
|
+
n.body !== void 0 && (c["Content-Type"] = "application/json", l = JSON.stringify(n.body)), n.idempotencyKey && (c["Idempotency-Key"] = n.idempotencyKey);
|
|
93
|
+
let u = o.has(e) || n.idempotencyKey !== void 0, d;
|
|
94
|
+
for (let t = 0;; t++) {
|
|
95
|
+
let n = t < s.length;
|
|
96
|
+
try {
|
|
97
|
+
d = await this.fetchImpl(i.toString(), {
|
|
98
|
+
method: e,
|
|
99
|
+
headers: c,
|
|
100
|
+
body: l
|
|
101
|
+
});
|
|
102
|
+
} catch (e) {
|
|
103
|
+
if (!u || !n) throw e;
|
|
104
|
+
await this.sleep(s[t]);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (!(u ? a.has(d.status) : d.status === 503) || !n) break;
|
|
108
|
+
await d.body?.cancel(), await this.sleep(s[t]);
|
|
109
|
+
}
|
|
110
|
+
if (d.status !== 204) {
|
|
111
|
+
if (!d.ok) {
|
|
78
112
|
let e;
|
|
79
113
|
try {
|
|
80
|
-
if (e = await
|
|
114
|
+
if (e = await d.json(), typeof e?.error != "string") throw Error("not an envelope");
|
|
81
115
|
} catch {
|
|
82
116
|
e = {
|
|
83
|
-
error: `http_${
|
|
84
|
-
message: `HTTP ${
|
|
117
|
+
error: `http_${d.status}`,
|
|
118
|
+
message: `HTTP ${d.status} from ${i.pathname}`
|
|
85
119
|
};
|
|
86
120
|
}
|
|
87
|
-
throw new r(
|
|
121
|
+
throw new r(d.status, e);
|
|
88
122
|
}
|
|
89
|
-
return await
|
|
123
|
+
return await d.json();
|
|
90
124
|
}
|
|
91
125
|
}
|
|
92
126
|
};
|
|
93
127
|
//#endregion
|
|
94
128
|
//#region src/errors.ts
|
|
95
|
-
function
|
|
129
|
+
function l(e) {
|
|
96
130
|
return { content: [{
|
|
97
131
|
type: "text",
|
|
98
132
|
text: JSON.stringify(e, null, 2)
|
|
99
133
|
}] };
|
|
100
134
|
}
|
|
101
|
-
function
|
|
135
|
+
function u(e) {
|
|
102
136
|
if (e instanceof r) {
|
|
103
137
|
let t = e.envelope, n = [`Featureflip API error ${e.status} (${t.error}): ${t.message}`];
|
|
104
138
|
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}`), {
|
|
@@ -117,33 +151,33 @@ function s(e) {
|
|
|
117
151
|
isError: !0
|
|
118
152
|
};
|
|
119
153
|
}
|
|
120
|
-
async function
|
|
154
|
+
async function d(e) {
|
|
121
155
|
try {
|
|
122
156
|
return await e();
|
|
123
157
|
} catch (e) {
|
|
124
|
-
return
|
|
158
|
+
return u(e);
|
|
125
159
|
}
|
|
126
160
|
}
|
|
127
161
|
//#endregion
|
|
128
162
|
//#region src/tools/projects.ts
|
|
129
|
-
var
|
|
163
|
+
var f = {
|
|
130
164
|
limit: t.number().int().min(1).max(100).optional().describe("Page size (default 20)"),
|
|
131
165
|
cursor: t.string().optional().describe("next_cursor from a previous response")
|
|
132
166
|
};
|
|
133
|
-
function
|
|
167
|
+
function p(e, n) {
|
|
134
168
|
e.registerTool("list_projects", {
|
|
135
169
|
title: "List projects",
|
|
136
170
|
description: "List projects in the Featureflip organization. Paginated: pass cursor from next_cursor to continue.",
|
|
137
|
-
inputSchema: t.object({ ...
|
|
171
|
+
inputSchema: t.object({ ...f }),
|
|
138
172
|
annotations: { readOnlyHint: !0 }
|
|
139
|
-
}, async ({ limit: e, cursor: t }) =>
|
|
173
|
+
}, async ({ limit: e, cursor: t }) => d(async () => l(await n.api.request("GET", `/api/v1/orgs/${i(n.org)}/projects`, { query: {
|
|
140
174
|
limit: e,
|
|
141
175
|
cursor: t
|
|
142
176
|
} }))));
|
|
143
177
|
}
|
|
144
178
|
//#endregion
|
|
145
179
|
//#region src/tools/environments.ts
|
|
146
|
-
function
|
|
180
|
+
function m(e, n) {
|
|
147
181
|
e.registerTool("list_environments", {
|
|
148
182
|
title: "List environments",
|
|
149
183
|
description: "List the environments of a project (e.g. development, staging, production).",
|
|
@@ -153,14 +187,14 @@ function d(e, n) {
|
|
|
153
187
|
cursor: t.string().optional()
|
|
154
188
|
}),
|
|
155
189
|
annotations: { readOnlyHint: !0 }
|
|
156
|
-
}, async ({ project: e, limit: t, cursor: r }) =>
|
|
190
|
+
}, 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: {
|
|
157
191
|
limit: t,
|
|
158
192
|
cursor: r
|
|
159
193
|
} }))));
|
|
160
194
|
}
|
|
161
195
|
//#endregion
|
|
162
196
|
//#region src/tools/segments.ts
|
|
163
|
-
function
|
|
197
|
+
function h(e, n) {
|
|
164
198
|
e.registerTool("list_segments", {
|
|
165
199
|
title: "List user segments",
|
|
166
200
|
description: "List reusable user segments in a project. Segments are referenced from targeting rules via userSegmentId.",
|
|
@@ -170,7 +204,7 @@ function f(e, n) {
|
|
|
170
204
|
cursor: t.string().optional()
|
|
171
205
|
}),
|
|
172
206
|
annotations: { readOnlyHint: !0 }
|
|
173
|
-
}, async ({ project: e, limit: t, cursor: r }) =>
|
|
207
|
+
}, 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: {
|
|
174
208
|
limit: t,
|
|
175
209
|
cursor: r
|
|
176
210
|
} })))), e.registerTool("get_segment", {
|
|
@@ -181,17 +215,17 @@ function f(e, n) {
|
|
|
181
215
|
segment: t.string().describe("Segment key or id")
|
|
182
216
|
}),
|
|
183
217
|
annotations: { readOnlyHint: !0 }
|
|
184
|
-
}, async ({ project: e, segment: t }) =>
|
|
218
|
+
}, 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)}`))));
|
|
185
219
|
}
|
|
186
220
|
//#endregion
|
|
187
221
|
//#region src/tools/flags.ts
|
|
188
|
-
var
|
|
222
|
+
var g = t.enum([
|
|
189
223
|
"Boolean",
|
|
190
224
|
"String",
|
|
191
225
|
"Number",
|
|
192
226
|
"Json"
|
|
193
227
|
]);
|
|
194
|
-
function
|
|
228
|
+
function _(e, n) {
|
|
195
229
|
let r = (e) => `/api/v1/orgs/${i(n.org)}/projects/${i(e)}/flags`;
|
|
196
230
|
e.registerTool("list_flags", {
|
|
197
231
|
title: "List feature flags",
|
|
@@ -200,13 +234,13 @@ function m(e, n) {
|
|
|
200
234
|
project: t.string().describe("Project key"),
|
|
201
235
|
search: t.string().optional(),
|
|
202
236
|
tag: t.string().optional(),
|
|
203
|
-
type:
|
|
237
|
+
type: g.optional(),
|
|
204
238
|
archived: t.boolean().optional().describe("true = only archived, false = only active"),
|
|
205
239
|
limit: t.number().int().min(1).max(100).optional(),
|
|
206
240
|
cursor: t.string().optional()
|
|
207
241
|
}),
|
|
208
242
|
annotations: { readOnlyHint: !0 }
|
|
209
|
-
}, async ({ project: e, ...t }) =>
|
|
243
|
+
}, async ({ project: e, ...t }) => d(async () => l(await n.api.request("GET", r(e), { query: t })))), e.registerTool("get_flag", {
|
|
210
244
|
title: "Get feature flag",
|
|
211
245
|
description: "Get one feature flag with its variations and metadata. Address by flag key or id.",
|
|
212
246
|
inputSchema: t.object({
|
|
@@ -214,14 +248,14 @@ function m(e, n) {
|
|
|
214
248
|
flag: t.string().describe("Flag key or id")
|
|
215
249
|
}),
|
|
216
250
|
annotations: { readOnlyHint: !0 }
|
|
217
|
-
}, async ({ project: e, flag: t }) =>
|
|
251
|
+
}, async ({ project: e, flag: t }) => d(async () => l(await n.api.request("GET", `${r(e)}/${i(t)}`)))), e.registerTool("create_flag", {
|
|
218
252
|
title: "Create feature flag",
|
|
219
253
|
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).",
|
|
220
254
|
inputSchema: t.object({
|
|
221
255
|
project: t.string().describe("Project key"),
|
|
222
256
|
key: t.string().describe("Unique flag key, e.g. checkout-v2"),
|
|
223
257
|
name: t.string(),
|
|
224
|
-
type:
|
|
258
|
+
type: g,
|
|
225
259
|
description: t.string().optional(),
|
|
226
260
|
tags: t.array(t.string()).optional(),
|
|
227
261
|
clientSideVisible: t.boolean().optional().describe("Expose to client-side SDKs (browser/mobile)"),
|
|
@@ -231,10 +265,11 @@ function m(e, n) {
|
|
|
231
265
|
value: t.string().describe("Variation value serialized as a string"),
|
|
232
266
|
description: t.string().optional()
|
|
233
267
|
})).optional(),
|
|
268
|
+
expiresAtUtc: t.string().optional().describe("Optional ISO-8601 UTC instant the flag is expected to be removed by (see set_flag_expiry)"),
|
|
234
269
|
idempotency_key: t.string().optional().describe("Idempotency-Key header for safe retries")
|
|
235
270
|
}),
|
|
236
271
|
annotations: { destructiveHint: !1 }
|
|
237
|
-
}, async ({ project: e, idempotency_key: t, ...i }) =>
|
|
272
|
+
}, async ({ project: e, idempotency_key: t, ...i }) => d(async () => l(await n.api.request("POST", r(e), {
|
|
238
273
|
body: i,
|
|
239
274
|
idempotencyKey: t
|
|
240
275
|
})))), e.registerTool("update_flag", {
|
|
@@ -249,11 +284,30 @@ function m(e, n) {
|
|
|
249
284
|
clientSideVisible: t.boolean().optional()
|
|
250
285
|
}),
|
|
251
286
|
annotations: { destructiveHint: !0 }
|
|
252
|
-
}, async ({ project: e, flag: t, ...a }) =>
|
|
287
|
+
}, async ({ project: e, flag: t, ...a }) => d(async () => (await n.api.request("PUT", `${r(e)}/${i(t)}`, { body: a }), l({
|
|
253
288
|
project: e,
|
|
254
289
|
flag: t,
|
|
255
290
|
updated: !0
|
|
256
|
-
})))), e.registerTool("
|
|
291
|
+
})))), e.registerTool("set_flag_expiry", {
|
|
292
|
+
title: "Set or clear flag expiry",
|
|
293
|
+
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 is an exact ISO-8601 UTC instant; a bare date means 00:00 UTC that day, so pass e.g. 2026-12-31T23:59:59Z for the end of a day. Refused with EXPIRY_IN_PAST for a time that has already passed, and with FEATURE_NOT_ENABLED while flag expiration is not enabled for the organization. Clearing is always allowed.",
|
|
294
|
+
inputSchema: t.object({
|
|
295
|
+
project: t.string(),
|
|
296
|
+
flag: t.string().describe("Flag key or id"),
|
|
297
|
+
expiresAtUtc: t.string().nullable().describe("ISO-8601 UTC instant, or null to clear the expiry")
|
|
298
|
+
}),
|
|
299
|
+
annotations: {
|
|
300
|
+
destructiveHint: !1,
|
|
301
|
+
idempotentHint: !0
|
|
302
|
+
}
|
|
303
|
+
}, async ({ project: e, flag: t, expiresAtUtc: a }) => d(async () => {
|
|
304
|
+
let o = `${r(e)}/${i(t)}/expiry`;
|
|
305
|
+
return a === null ? await n.api.request("DELETE", o) : await n.api.request("PUT", o, { body: { expiresAtUtc: a } }), l({
|
|
306
|
+
project: e,
|
|
307
|
+
flag: t,
|
|
308
|
+
expiresAtUtc: a
|
|
309
|
+
});
|
|
310
|
+
})), e.registerTool("delete_flag", {
|
|
257
311
|
title: "Delete feature flag",
|
|
258
312
|
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.",
|
|
259
313
|
inputSchema: t.object({
|
|
@@ -261,15 +315,16 @@ function m(e, n) {
|
|
|
261
315
|
flag: t.string().describe("Flag key or id")
|
|
262
316
|
}),
|
|
263
317
|
annotations: { destructiveHint: !0 }
|
|
264
|
-
}, async ({ project: e, flag: t }) =>
|
|
318
|
+
}, async ({ project: e, flag: t }) => d(async () => (await n.api.request("DELETE", `${r(e)}/${i(t)}`), l({ deleted: t })))), e.registerTool("archive_flag", {
|
|
265
319
|
title: "Archive feature flag",
|
|
266
|
-
description: "Archive a flag (soft-hide, evaluation stops serving it). Reversible with restore_flag.",
|
|
320
|
+
description: "Archive a flag (soft-hide, evaluation stops serving it). Reversible with restore_flag. 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.",
|
|
267
321
|
inputSchema: t.object({
|
|
268
322
|
project: t.string(),
|
|
269
|
-
flag: t.string()
|
|
323
|
+
flag: t.string(),
|
|
324
|
+
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.")
|
|
270
325
|
}),
|
|
271
326
|
annotations: { destructiveHint: !0 }
|
|
272
|
-
}, async ({ project: e, flag: t }) =>
|
|
327
|
+
}, async ({ project: e, flag: t, force: a }) => d(async () => (await n.api.request("POST", `${r(e)}/${i(t)}/archive`, { query: a ? { force: !0 } : {} }), l({
|
|
273
328
|
flag: t,
|
|
274
329
|
archived: !0
|
|
275
330
|
})))), e.registerTool("restore_flag", {
|
|
@@ -280,19 +335,19 @@ function m(e, n) {
|
|
|
280
335
|
flag: t.string()
|
|
281
336
|
}),
|
|
282
337
|
annotations: { destructiveHint: !1 }
|
|
283
|
-
}, async ({ project: e, flag: t }) =>
|
|
338
|
+
}, async ({ project: e, flag: t }) => d(async () => (await n.api.request("POST", `${r(e)}/${i(t)}/restore`), l({
|
|
284
339
|
flag: t,
|
|
285
340
|
archived: !1
|
|
286
341
|
}))));
|
|
287
342
|
}
|
|
288
343
|
//#endregion
|
|
289
344
|
//#region src/tools/flag-environments.ts
|
|
290
|
-
var
|
|
345
|
+
var v = t.enum([
|
|
291
346
|
"SingleVariation",
|
|
292
347
|
"PercentageRollout",
|
|
293
348
|
"TargetedRollout"
|
|
294
349
|
]);
|
|
295
|
-
function
|
|
350
|
+
function y(e, n) {
|
|
296
351
|
let r = (e, t) => `/api/v1/orgs/${i(n.org)}/projects/${i(e)}/flags/${i(t)}`;
|
|
297
352
|
e.registerTool("toggle_flag", {
|
|
298
353
|
title: "Toggle flag in environment",
|
|
@@ -304,11 +359,11 @@ function g(e, n) {
|
|
|
304
359
|
enabled: t.boolean()
|
|
305
360
|
}),
|
|
306
361
|
annotations: { destructiveHint: !0 }
|
|
307
|
-
}, async ({ project: e, flag: t, environment: a, enabled:
|
|
362
|
+
}, 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({
|
|
308
363
|
project: e,
|
|
309
364
|
flag: t,
|
|
310
365
|
environment: a,
|
|
311
|
-
enabled:
|
|
366
|
+
enabled: o
|
|
312
367
|
})))), e.registerTool("update_flag_environment_config", {
|
|
313
368
|
title: "Update flag environment config",
|
|
314
369
|
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.",
|
|
@@ -317,18 +372,18 @@ function g(e, n) {
|
|
|
317
372
|
flag: t.string(),
|
|
318
373
|
environment: t.string(),
|
|
319
374
|
defaultVariationId: t.string().describe("Variation id to serve on fallthrough — required by the API"),
|
|
320
|
-
strategy:
|
|
375
|
+
strategy: v.optional(),
|
|
321
376
|
prerequisites: t.array(t.object({
|
|
322
377
|
prerequisiteFlagKey: t.string(),
|
|
323
378
|
expectedVariationKey: t.string()
|
|
324
379
|
})).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.")
|
|
325
380
|
}),
|
|
326
381
|
annotations: { destructiveHint: !0 }
|
|
327
|
-
}, async ({ project: e, flag: t, environment: a, ...
|
|
382
|
+
}, async ({ project: e, flag: t, environment: a, ...o }) => d(async () => (await n.api.request("PUT", `${r(e, t)}/environments/${i(a)}`, { body: o }), l({
|
|
328
383
|
project: e,
|
|
329
384
|
flag: t,
|
|
330
385
|
environment: a,
|
|
331
|
-
...
|
|
386
|
+
...o
|
|
332
387
|
})))), e.registerTool("flag_status", {
|
|
333
388
|
title: "Flag status across environments",
|
|
334
389
|
description: "Compact cross-environment view of one flag: enabled state, strategy, default variation, and prerequisites per environment.",
|
|
@@ -337,9 +392,9 @@ function g(e, n) {
|
|
|
337
392
|
flag: t.string()
|
|
338
393
|
}),
|
|
339
394
|
annotations: { readOnlyHint: !0 }
|
|
340
|
-
}, async ({ project: e, flag: t }) =>
|
|
341
|
-
let [i, a] = await Promise.all([n.api.request("GET", r(e, t)), n.api.request("GET", `${r(e, t)}/environments`)]),
|
|
342
|
-
return
|
|
395
|
+
}, async ({ project: e, flag: t }) => d(async () => {
|
|
396
|
+
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;
|
|
397
|
+
return l({
|
|
343
398
|
key: i.key ?? null,
|
|
344
399
|
name: i.name ?? null,
|
|
345
400
|
type: i.type ?? null,
|
|
@@ -349,7 +404,7 @@ function g(e, n) {
|
|
|
349
404
|
environment: e.environmentKey ?? null,
|
|
350
405
|
enabled: e.isEnabled ?? !1,
|
|
351
406
|
strategy: e.strategy ?? null,
|
|
352
|
-
defaultVariation:
|
|
407
|
+
defaultVariation: o(e.defaultVariationId),
|
|
353
408
|
prerequisites: e.prerequisites ?? []
|
|
354
409
|
}))
|
|
355
410
|
});
|
|
@@ -357,22 +412,22 @@ function g(e, n) {
|
|
|
357
412
|
}
|
|
358
413
|
//#endregion
|
|
359
414
|
//#region src/tools/targeting.ts
|
|
360
|
-
var
|
|
415
|
+
var b = t.object({
|
|
361
416
|
attribute: t.string().describe("Context attribute name, e.g. plan, country, user_id"),
|
|
362
417
|
operator: t.string().describe("OperatorType member name, case-sensitive — e.g. Equals, NotEquals, Contains, In, GreaterThan"),
|
|
363
418
|
values: t.array(t.string()),
|
|
364
419
|
negate: t.boolean().optional().default(!1)
|
|
365
|
-
}),
|
|
420
|
+
}), x = t.object({
|
|
366
421
|
description: t.string().optional(),
|
|
367
422
|
variationId: t.string().optional().describe("Variation served when the rule matches"),
|
|
368
423
|
rolloutPercentage: t.number().int().min(0).max(100).optional().describe("Percentage rollout for this rule"),
|
|
369
424
|
userSegmentId: t.string().optional().describe("Match users in this segment (see list_segments)"),
|
|
370
425
|
conditionGroups: t.array(t.object({
|
|
371
426
|
operator: t.enum(["And", "Or"]).describe("How conditions inside this group combine"),
|
|
372
|
-
conditions: t.array(
|
|
427
|
+
conditions: t.array(b)
|
|
373
428
|
})).optional().describe("Groups are ANDed together; conditions inside a group use the group operator")
|
|
374
429
|
});
|
|
375
|
-
function
|
|
430
|
+
function S(e, n) {
|
|
376
431
|
let r = (e, t, r) => `/api/v1/orgs/${i(n.org)}/projects/${i(e)}/flags/${i(t)}/environments/${i(r)}/targeting`;
|
|
377
432
|
e.registerTool("get_targeting", {
|
|
378
433
|
title: "Get targeting rules",
|
|
@@ -383,17 +438,17 @@ function y(e, n) {
|
|
|
383
438
|
environment: t.string()
|
|
384
439
|
}),
|
|
385
440
|
annotations: { readOnlyHint: !0 }
|
|
386
|
-
}, async ({ project: e, flag: t, environment: i }) =>
|
|
441
|
+
}, async ({ project: e, flag: t, environment: i }) => d(async () => l(await n.api.request("GET", r(e, t, i))))), e.registerTool("update_targeting", {
|
|
387
442
|
title: "Replace targeting rules",
|
|
388
443
|
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.",
|
|
389
444
|
inputSchema: t.object({
|
|
390
445
|
project: t.string(),
|
|
391
446
|
flag: t.string(),
|
|
392
447
|
environment: t.string(),
|
|
393
|
-
rules: t.array(
|
|
448
|
+
rules: t.array(x)
|
|
394
449
|
}),
|
|
395
450
|
annotations: { destructiveHint: !0 }
|
|
396
|
-
}, async ({ project: e, flag: t, environment: i, rules: a }) =>
|
|
451
|
+
}, async ({ project: e, flag: t, environment: i, rules: a }) => d(async () => (await n.api.request("PUT", r(e, t, i), { body: { rules: a } }), l({
|
|
397
452
|
project: e,
|
|
398
453
|
flag: t,
|
|
399
454
|
environment: i,
|
|
@@ -402,7 +457,7 @@ function y(e, n) {
|
|
|
402
457
|
}
|
|
403
458
|
//#endregion
|
|
404
459
|
//#region src/tools/variations.ts
|
|
405
|
-
function
|
|
460
|
+
function C(e, n) {
|
|
406
461
|
e.registerTool("manage_variation", {
|
|
407
462
|
title: "Add, update, or remove a variation",
|
|
408
463
|
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.",
|
|
@@ -421,68 +476,213 @@ function b(e, n) {
|
|
|
421
476
|
description: t.string().optional()
|
|
422
477
|
}),
|
|
423
478
|
annotations: { destructiveHint: !0 }
|
|
424
|
-
}, async ({ project: e, flag: t, action: r, variationId: a, key:
|
|
425
|
-
let
|
|
426
|
-
return r === "add" ? !
|
|
427
|
-
key:
|
|
428
|
-
name:
|
|
429
|
-
value:
|
|
479
|
+
}, async ({ project: e, flag: t, action: r, variationId: a, key: o, name: s, value: c, description: f }) => d(async () => {
|
|
480
|
+
let d = `/api/v1/orgs/${i(n.org)}/projects/${i(e)}/flags/${i(t)}/variations`;
|
|
481
|
+
return r === "add" ? !o || c === void 0 ? u(/* @__PURE__ */ Error("action=add requires key and value")) : l(await n.api.request("POST", d, { body: {
|
|
482
|
+
key: o,
|
|
483
|
+
name: s,
|
|
484
|
+
value: c,
|
|
430
485
|
description: f
|
|
431
|
-
} })) : a ? r === "update" ? (await n.api.request("PUT", `${
|
|
432
|
-
name:
|
|
433
|
-
value:
|
|
486
|
+
} })) : a ? r === "update" ? (await n.api.request("PUT", `${d}/${i(a)}`, { body: {
|
|
487
|
+
name: s,
|
|
488
|
+
value: c,
|
|
434
489
|
description: f
|
|
435
|
-
} }),
|
|
490
|
+
} }), 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)`));
|
|
436
491
|
}));
|
|
437
492
|
}
|
|
438
493
|
//#endregion
|
|
494
|
+
//#region src/tools/webhooks.ts
|
|
495
|
+
var w = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, T = "Requires an Admin token. A 404 on every webhook call means webhooks are not enabled for the organization.", E = {
|
|
496
|
+
limit: t.number().int().min(1).max(100).optional().describe("Page size (default 20)"),
|
|
497
|
+
cursor: t.string().optional().describe("next_cursor from a previous response")
|
|
498
|
+
};
|
|
499
|
+
function D(e, n) {
|
|
500
|
+
let r = `/api/v1/orgs/${i(n.org)}/webhooks`, a = (e) => `${r}/${i(e)}`;
|
|
501
|
+
async function o(e) {
|
|
502
|
+
let t = [];
|
|
503
|
+
for (let r of e) {
|
|
504
|
+
if (w.test(r)) {
|
|
505
|
+
t.push(r);
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
let e = await n.api.request("GET", `/api/v1/orgs/${i(n.org)}/projects/${i(r)}`);
|
|
509
|
+
if (!e?.id) throw Error(`Project "${r}" resolved without an id`);
|
|
510
|
+
t.push(e.id);
|
|
511
|
+
}
|
|
512
|
+
return t;
|
|
513
|
+
}
|
|
514
|
+
async function s(e) {
|
|
515
|
+
let t = e.map((e) => {
|
|
516
|
+
if (w.test(e)) return { id: e };
|
|
517
|
+
let t = e.indexOf("/"), n = e.slice(0, t), r = e.slice(t + 1);
|
|
518
|
+
if (t < 0 || !n || !r) throw Error(`Environment "${e}" needs its project: pass "<project>/<environment>", e.g. "web/production", or the environment's id.`);
|
|
519
|
+
return {
|
|
520
|
+
project: n,
|
|
521
|
+
env: r
|
|
522
|
+
};
|
|
523
|
+
}), r = [];
|
|
524
|
+
for (let e of t) {
|
|
525
|
+
if ("id" in e) {
|
|
526
|
+
r.push(e.id);
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
let t = await n.api.request("GET", `/api/v1/orgs/${i(n.org)}/projects/${i(e.project)}/environments/${i(e.env)}`);
|
|
530
|
+
if (!t?.id) throw Error(`Environment "${e.project}/${e.env}" resolved without an id`);
|
|
531
|
+
r.push(t.id);
|
|
532
|
+
}
|
|
533
|
+
return r;
|
|
534
|
+
}
|
|
535
|
+
e.registerTool("list_webhooks", {
|
|
536
|
+
title: "List webhook subscriptions",
|
|
537
|
+
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. " + T,
|
|
538
|
+
inputSchema: t.object({ ...E }),
|
|
539
|
+
annotations: { readOnlyHint: !0 }
|
|
540
|
+
}, async ({ limit: e, cursor: t }) => d(async () => {
|
|
541
|
+
let [i, a] = await Promise.all([n.api.request("GET", r, { query: {
|
|
542
|
+
limit: e,
|
|
543
|
+
cursor: t
|
|
544
|
+
} }), n.api.request("GET", `${r}/event-types`)]);
|
|
545
|
+
return l({
|
|
546
|
+
...i,
|
|
547
|
+
availableEventTypes: a
|
|
548
|
+
});
|
|
549
|
+
})), e.registerTool("list_webhook_deliveries", {
|
|
550
|
+
title: "List webhook deliveries",
|
|
551
|
+
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. " + T,
|
|
552
|
+
inputSchema: t.object({
|
|
553
|
+
id: t.string().describe("Subscription id (from list_webhooks)"),
|
|
554
|
+
...E
|
|
555
|
+
}),
|
|
556
|
+
annotations: { readOnlyHint: !0 }
|
|
557
|
+
}, async ({ id: e, limit: t, cursor: r }) => d(async () => l(await n.api.request("GET", `${a(e)}/deliveries`, { query: {
|
|
558
|
+
limit: t,
|
|
559
|
+
cursor: r
|
|
560
|
+
} })))), e.registerTool("manage_webhook", {
|
|
561
|
+
title: "Create, update, or delete a webhook subscription, or rotate its secret",
|
|
562
|
+
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. " + T,
|
|
563
|
+
inputSchema: t.object({
|
|
564
|
+
action: t.enum([
|
|
565
|
+
"create",
|
|
566
|
+
"update",
|
|
567
|
+
"delete",
|
|
568
|
+
"rotate_secret",
|
|
569
|
+
"retire_secret"
|
|
570
|
+
]),
|
|
571
|
+
id: t.string().optional().describe("Subscription id; required for everything except create"),
|
|
572
|
+
name: t.string().optional().describe("Required for create"),
|
|
573
|
+
url: t.string().optional().describe("Receiver URL; required for create"),
|
|
574
|
+
eventTypes: t.array(t.string()).optional().describe("Event types to send; empty or omitted = all"),
|
|
575
|
+
projects: t.array(t.string()).optional().describe("Project keys or ids to cover; empty or omitted on create = all"),
|
|
576
|
+
environments: t.array(t.string()).optional().describe("\"<project>/<environment>\" keys or environment ids; empty or omitted on create = all"),
|
|
577
|
+
enabled: t.boolean().optional().describe("update only; subscriptions are created enabled"),
|
|
578
|
+
secretId: t.string().optional().describe("Required for retire_secret"),
|
|
579
|
+
idempotency_key: t.string().optional().describe("Idempotency-Key header for create / rotate_secret")
|
|
580
|
+
}),
|
|
581
|
+
annotations: { destructiveHint: !0 }
|
|
582
|
+
}, async ({ action: e, id: t, name: c, url: f, eventTypes: p, projects: m, environments: h, enabled: g, secretId: _, idempotency_key: v }) => d(async () => {
|
|
583
|
+
if (e === "create") {
|
|
584
|
+
if (!c || !f) return u(/* @__PURE__ */ Error("action=create requires name and url"));
|
|
585
|
+
let e = h && await s(h), t = m && await o(m);
|
|
586
|
+
return l(await n.api.request("POST", r, {
|
|
587
|
+
body: {
|
|
588
|
+
name: c,
|
|
589
|
+
url: f,
|
|
590
|
+
provider: "GenericHttp",
|
|
591
|
+
eventTypes: p,
|
|
592
|
+
projectIds: t,
|
|
593
|
+
environmentIds: e
|
|
594
|
+
},
|
|
595
|
+
idempotencyKey: v
|
|
596
|
+
}));
|
|
597
|
+
}
|
|
598
|
+
if (!t) return u(/* @__PURE__ */ Error(`action=${e} requires id (see list_webhooks)`));
|
|
599
|
+
if (e === "update") {
|
|
600
|
+
let e = h && await s(h), r = m && await o(m), i = await n.api.request("GET", a(t)), u = {
|
|
601
|
+
name: c ?? i.name,
|
|
602
|
+
url: f ?? i.url,
|
|
603
|
+
eventTypes: p ?? i.eventTypes,
|
|
604
|
+
projectIds: r ?? i.projectIds,
|
|
605
|
+
environmentIds: e ?? i.environmentIds,
|
|
606
|
+
isEnabled: g ?? i.isEnabled
|
|
607
|
+
};
|
|
608
|
+
return await n.api.request("PUT", a(t), { body: u }), l({
|
|
609
|
+
id: t,
|
|
610
|
+
updated: !0,
|
|
611
|
+
...u
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
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({
|
|
615
|
+
id: t,
|
|
616
|
+
retiredSecretId: _
|
|
617
|
+
})) : u(/* @__PURE__ */ Error("action=retire_secret requires secretId (see list_webhooks)"));
|
|
618
|
+
})), e.registerTool("deliver_webhook", {
|
|
619
|
+
title: "Send a test event or redeliver",
|
|
620
|
+
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. " + T,
|
|
621
|
+
inputSchema: t.object({
|
|
622
|
+
action: t.enum(["test", "redeliver"]),
|
|
623
|
+
id: t.string().describe("Subscription id (from list_webhooks)"),
|
|
624
|
+
deliveryId: t.string().optional().describe("Required for redeliver")
|
|
625
|
+
}),
|
|
626
|
+
annotations: {
|
|
627
|
+
destructiveHint: !1,
|
|
628
|
+
openWorldHint: !0
|
|
629
|
+
}
|
|
630
|
+
}, 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({
|
|
631
|
+
id: t,
|
|
632
|
+
redelivered: r
|
|
633
|
+
})) : u(/* @__PURE__ */ Error("action=redeliver requires deliveryId (see list_webhook_deliveries)"))));
|
|
634
|
+
}
|
|
635
|
+
//#endregion
|
|
439
636
|
//#region src/tools/workflows/stale-flags.ts
|
|
440
|
-
var
|
|
441
|
-
function
|
|
637
|
+
var O = 50;
|
|
638
|
+
function k(e, n) {
|
|
442
639
|
e.registerTool("find_stale_flags", {
|
|
443
640
|
title: "Find stale flags",
|
|
444
|
-
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). Checks at most ${
|
|
641
|
+
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 and expired. Checks at most ${O} candidates per call, expired flags first.`,
|
|
445
642
|
inputSchema: t.object({
|
|
446
643
|
project: t.string(),
|
|
447
644
|
days: t.number().int().min(1).optional().default(30).describe("Minimum age in days since last update")
|
|
448
645
|
}),
|
|
449
646
|
annotations: { readOnlyHint: !0 }
|
|
450
|
-
}, async ({ project: e, days: t }) =>
|
|
451
|
-
let r = `/api/v1/orgs/${i(n.org)}/projects/${i(e)}/flags`, a = Date.now() - t * 24 * 60 * 60 * 1e3, s = [],
|
|
647
|
+
}, async ({ project: e, days: t }) => d(async () => {
|
|
648
|
+
let r = `/api/v1/orgs/${i(n.org)}/projects/${i(e)}/flags`, a = Date.now(), o = a - t * 24 * 60 * 60 * 1e3, s = (e) => !!e.expiresAtUtc && new Date(e.expiresAtUtc).getTime() <= a, c = [], u;
|
|
452
649
|
do {
|
|
453
650
|
let e = await n.api.request("GET", r, { query: {
|
|
454
651
|
archived: !1,
|
|
455
652
|
limit: 100,
|
|
456
|
-
cursor:
|
|
653
|
+
cursor: u
|
|
457
654
|
} });
|
|
458
|
-
|
|
459
|
-
} while (
|
|
460
|
-
|
|
461
|
-
|
|
655
|
+
c.push(...(e.items ?? []).filter((e) => !!e.key && !!e.name && !!e.updatedAt && (new Date(e.updatedAt).getTime() < o || s(e)))), u = e.next_cursor ?? void 0;
|
|
656
|
+
} while (u);
|
|
657
|
+
c.sort((e, t) => Number(s(t)) - Number(s(e)));
|
|
658
|
+
let d = c.length > O, f = c.slice(0, O), p = [];
|
|
659
|
+
for (let e of f) {
|
|
462
660
|
let t = await n.api.request("GET", `${r}/${i(e.key)}/environments`);
|
|
463
661
|
if (t.length === 0) continue;
|
|
464
|
-
let a = t.every((e) => e.isEnabled),
|
|
465
|
-
(
|
|
662
|
+
let a = s(e), o = t.every((e) => e.isEnabled), c = t.every((e) => !e.isEnabled);
|
|
663
|
+
(o || c || a) && p.push({
|
|
466
664
|
key: e.key,
|
|
467
665
|
name: e.name,
|
|
468
666
|
updatedAt: e.updatedAt,
|
|
469
|
-
|
|
667
|
+
expiresAtUtc: e.expiresAtUtc ?? null,
|
|
668
|
+
expired: a,
|
|
669
|
+
reason: o ? "enabled-everywhere" : c ? "disabled-everywhere" : "past-expiry",
|
|
470
670
|
environments: t.length
|
|
471
671
|
});
|
|
472
672
|
}
|
|
473
|
-
return
|
|
673
|
+
return l({
|
|
474
674
|
project: e,
|
|
475
675
|
olderThanDays: t,
|
|
476
|
-
checked:
|
|
477
|
-
truncated:
|
|
478
|
-
...
|
|
479
|
-
stale:
|
|
676
|
+
checked: f.length,
|
|
677
|
+
truncated: d,
|
|
678
|
+
...d ? { note: `Only the first ${O} of ${c.length} candidates were checked.` } : {},
|
|
679
|
+
stale: p
|
|
480
680
|
});
|
|
481
681
|
}));
|
|
482
682
|
}
|
|
483
683
|
//#endregion
|
|
484
684
|
//#region src/tools/workflows/snippets.ts
|
|
485
|
-
var
|
|
685
|
+
var A = [
|
|
486
686
|
"js",
|
|
487
687
|
"node",
|
|
488
688
|
"browser",
|
|
@@ -494,16 +694,16 @@ var C = [
|
|
|
494
694
|
"php",
|
|
495
695
|
"ruby",
|
|
496
696
|
"swift"
|
|
497
|
-
],
|
|
697
|
+
], j = [
|
|
498
698
|
"browser",
|
|
499
699
|
"react",
|
|
500
700
|
"swift"
|
|
501
701
|
];
|
|
502
|
-
function
|
|
503
|
-
return
|
|
702
|
+
function M(e) {
|
|
703
|
+
return j.includes(e);
|
|
504
704
|
}
|
|
505
|
-
var
|
|
506
|
-
function
|
|
705
|
+
var N = "Uses a SERVER-side SDK key (Featureflip dashboard → environment → SDK keys).", P = "Uses a CLIENT-side SDK key (safe to ship to browsers/apps).";
|
|
706
|
+
function F(e, t) {
|
|
507
707
|
switch (e) {
|
|
508
708
|
case "js": return {
|
|
509
709
|
install: "npm install @featureflip/js",
|
|
@@ -518,7 +718,7 @@ const enabled = client.boolVariation('${t}', { user_id: userId }, false);
|
|
|
518
718
|
if (enabled) {
|
|
519
719
|
// new code path
|
|
520
720
|
}`,
|
|
521
|
-
notes:
|
|
721
|
+
notes: N
|
|
522
722
|
};
|
|
523
723
|
case "node": return {
|
|
524
724
|
install: "npm install @featureflip/node",
|
|
@@ -529,7 +729,7 @@ const enabled = client.boolVariation('${t}', { user_id: userId }, false);
|
|
|
529
729
|
if (enabled) {
|
|
530
730
|
// new code path
|
|
531
731
|
}`,
|
|
532
|
-
notes:
|
|
732
|
+
notes: N
|
|
533
733
|
};
|
|
534
734
|
case "browser": return {
|
|
535
735
|
install: "npm install @featureflip/browser",
|
|
@@ -541,7 +741,7 @@ const enabled = client.boolVariation('${t}', false);
|
|
|
541
741
|
if (enabled) {
|
|
542
742
|
// new code path
|
|
543
743
|
}`,
|
|
544
|
-
notes:
|
|
744
|
+
notes: P
|
|
545
745
|
};
|
|
546
746
|
case "react": return {
|
|
547
747
|
install: "npm install @featureflip/react @featureflip/browser",
|
|
@@ -554,7 +754,7 @@ function MyComponent() {
|
|
|
554
754
|
const enabled = useFeatureFlag('${t}', false);
|
|
555
755
|
return enabled ? <NewExperience /> : <CurrentExperience />;
|
|
556
756
|
}`,
|
|
557
|
-
notes:
|
|
757
|
+
notes: P
|
|
558
758
|
};
|
|
559
759
|
case "python": return {
|
|
560
760
|
install: "pip install featureflip",
|
|
@@ -565,7 +765,7 @@ enabled = client.variation("${t}", {"user_id": user_id}, default=False)
|
|
|
565
765
|
if enabled:
|
|
566
766
|
# new code path
|
|
567
767
|
...`,
|
|
568
|
-
notes:
|
|
768
|
+
notes: N
|
|
569
769
|
};
|
|
570
770
|
case "csharp": return {
|
|
571
771
|
install: "dotnet add package Featureflip.Client",
|
|
@@ -578,7 +778,7 @@ if (enabled)
|
|
|
578
778
|
{
|
|
579
779
|
// new code path
|
|
580
780
|
}`,
|
|
581
|
-
notes:
|
|
781
|
+
notes: N
|
|
582
782
|
};
|
|
583
783
|
case "java": return {
|
|
584
784
|
install: "implementation 'io.featureflip:featureflip-java:2.0.0'",
|
|
@@ -592,7 +792,7 @@ boolean enabled = client.boolVariation("${t}",
|
|
|
592
792
|
if (enabled) {
|
|
593
793
|
// new code path
|
|
594
794
|
}`,
|
|
595
|
-
notes:
|
|
795
|
+
notes: N
|
|
596
796
|
};
|
|
597
797
|
case "go": return {
|
|
598
798
|
install: "go get github.com/canopy-labs/featureflip-go/v2",
|
|
@@ -608,7 +808,7 @@ ctx := featureflip.EvaluationContext{UserID: userID}
|
|
|
608
808
|
if client.BoolVariation("${t}", ctx, false) {
|
|
609
809
|
// new code path
|
|
610
810
|
}`,
|
|
611
|
-
notes:
|
|
811
|
+
notes: N
|
|
612
812
|
};
|
|
613
813
|
case "php": return {
|
|
614
814
|
install: "composer require featureflip/featureflip-php",
|
|
@@ -623,7 +823,7 @@ $enabled = $client->boolVariation('${t}', ['user_id' => $userId], false);
|
|
|
623
823
|
if ($enabled) {
|
|
624
824
|
// new code path
|
|
625
825
|
}`,
|
|
626
|
-
notes:
|
|
826
|
+
notes: N + " Requires PSR-16 cache and PSR-18 HTTP implementations."
|
|
627
827
|
};
|
|
628
828
|
case "ruby": return {
|
|
629
829
|
install: "gem \"featureflip\"",
|
|
@@ -634,7 +834,7 @@ enabled = client.bool_variation("${t}", { "user_id" => user_id }, false)
|
|
|
634
834
|
if enabled
|
|
635
835
|
# new code path
|
|
636
836
|
end`,
|
|
637
|
-
notes:
|
|
837
|
+
notes: N
|
|
638
838
|
};
|
|
639
839
|
case "swift": return {
|
|
640
840
|
install: ".package(url: \"https://github.com/canopy-labs/featureflip-swift\", from: \"2.0.0\")",
|
|
@@ -647,13 +847,13 @@ let enabled = client.boolVariation("${t}", default: false)
|
|
|
647
847
|
if enabled {
|
|
648
848
|
// new code path
|
|
649
849
|
}`,
|
|
650
|
-
notes:
|
|
850
|
+
notes: P
|
|
651
851
|
};
|
|
652
852
|
}
|
|
653
853
|
}
|
|
654
854
|
//#endregion
|
|
655
855
|
//#region src/tools/workflows/wrap-feature.ts
|
|
656
|
-
function
|
|
856
|
+
function I(e, n) {
|
|
657
857
|
e.registerTool("wrap_feature", {
|
|
658
858
|
title: "Wrap a feature in a new flag",
|
|
659
859
|
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.",
|
|
@@ -663,26 +863,26 @@ function k(e, n) {
|
|
|
663
863
|
name: t.string().optional().describe("Display name; defaults to the key"),
|
|
664
864
|
description: t.string().optional().describe("What this flag guards"),
|
|
665
865
|
tags: t.array(t.string()).optional(),
|
|
666
|
-
language: t.enum(
|
|
866
|
+
language: t.enum(A).describe("SDK language of the codebase being edited"),
|
|
667
867
|
idempotency_key: t.string().optional()
|
|
668
868
|
}),
|
|
669
869
|
annotations: { destructiveHint: !1 }
|
|
670
|
-
}, async ({ project: e, key: t, name: r, description: a, tags:
|
|
671
|
-
let
|
|
870
|
+
}, async ({ project: e, key: t, name: r, description: a, tags: o, language: s, idempotency_key: c }) => d(async () => {
|
|
871
|
+
let u = await n.api.request("POST", `/api/v1/orgs/${i(n.org)}/projects/${i(e)}/flags`, {
|
|
672
872
|
body: {
|
|
673
873
|
key: t,
|
|
674
874
|
name: r ?? t,
|
|
675
875
|
type: "Boolean",
|
|
676
876
|
description: a,
|
|
677
|
-
tags:
|
|
678
|
-
...
|
|
877
|
+
tags: o,
|
|
878
|
+
...M(s) ? { clientSideVisible: !0 } : {}
|
|
679
879
|
},
|
|
680
|
-
idempotencyKey:
|
|
681
|
-
}), d =
|
|
682
|
-
return
|
|
683
|
-
flag:
|
|
880
|
+
idempotencyKey: c
|
|
881
|
+
}), d = F(s, t);
|
|
882
|
+
return l({
|
|
883
|
+
flag: u,
|
|
684
884
|
integration: {
|
|
685
|
-
language:
|
|
885
|
+
language: s,
|
|
686
886
|
install: d.install,
|
|
687
887
|
code: d.code,
|
|
688
888
|
notes: d.notes
|
|
@@ -697,17 +897,17 @@ function k(e, n) {
|
|
|
697
897
|
}
|
|
698
898
|
//#endregion
|
|
699
899
|
//#region src/server.ts
|
|
700
|
-
function
|
|
900
|
+
function L(t) {
|
|
701
901
|
let r = new e({
|
|
702
902
|
name: "featureflip",
|
|
703
903
|
version: n
|
|
704
904
|
});
|
|
705
|
-
return
|
|
905
|
+
return p(r, t), m(r, t), h(r, t), _(r, t), y(r, t), S(r, t), C(r, t), D(r, t), k(r, t), I(r, t), r;
|
|
706
906
|
}
|
|
707
907
|
//#endregion
|
|
708
908
|
//#region src/config.ts
|
|
709
|
-
var
|
|
710
|
-
function
|
|
909
|
+
var R = "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.";
|
|
910
|
+
function z(e = process.env) {
|
|
711
911
|
let t = e.FEATUREFLIP_TOKEN?.trim();
|
|
712
912
|
return t ? {
|
|
713
913
|
token: t,
|
|
@@ -715,14 +915,14 @@ function M(e = process.env) {
|
|
|
715
915
|
org: e.FEATUREFLIP_ORG?.trim() || void 0
|
|
716
916
|
} : null;
|
|
717
917
|
}
|
|
718
|
-
function
|
|
719
|
-
let t =
|
|
720
|
-
if (!t) throw Error(
|
|
918
|
+
function B(e = process.env) {
|
|
919
|
+
let t = z(e);
|
|
920
|
+
if (!t) throw Error(R);
|
|
721
921
|
return t;
|
|
722
922
|
}
|
|
723
923
|
//#endregion
|
|
724
924
|
//#region src/org.ts
|
|
725
|
-
async function
|
|
925
|
+
async function V(e, t) {
|
|
726
926
|
if (t) return t;
|
|
727
927
|
let n = (await e.request("GET", "/api/v1/orgs", { query: { limit: 100 } })).items ?? [];
|
|
728
928
|
if (n.length === 1) {
|
|
@@ -733,4 +933,4 @@ async function P(e, t) {
|
|
|
733
933
|
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(", "));
|
|
734
934
|
}
|
|
735
935
|
//#endregion
|
|
736
|
-
export {
|
|
936
|
+
export { L as a, n as c, z as i, R as n, r as o, B as r, c as s, V as t };
|