@featureflip/mcp 0.1.7 → 0.1.9
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 +5 -2
- 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 +2404 -494
- package/dist/generated/api-types.d.ts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{org-Fbj4wFOi.js → org-8tn1Su68.js} +388 -142
- package/dist/server.d.ts.map +1 -1
- package/dist/tools/flags.d.ts.map +1 -1
- package/dist/tools/owner.d.ts +10 -0
- package/dist/tools/owner.d.ts.map +1 -0
- 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 +19 -7
|
@@ -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.9",
|
|
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",
|
|
@@ -31,17 +43,17 @@ var n = {
|
|
|
31
43
|
"smoke:docker": "node scripts/docker-smoke.mjs"
|
|
32
44
|
},
|
|
33
45
|
dependencies: {
|
|
34
|
-
"@modelcontextprotocol/server": "2.
|
|
46
|
+
"@modelcontextprotocol/server": "2.1.0",
|
|
35
47
|
zod: "^4.6.5"
|
|
36
48
|
},
|
|
37
49
|
devDependencies: {
|
|
38
|
-
"@modelcontextprotocol/client": "2.
|
|
50
|
+
"@modelcontextprotocol/client": "2.1.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
|
-
vite: "^8.3.
|
|
43
|
-
"vite-plugin-dts": "^5.1.
|
|
44
|
-
vitest: "^5.0.
|
|
54
|
+
vite: "^8.3.1",
|
|
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,47 +215,62 @@ 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)}`))));
|
|
219
|
+
}
|
|
220
|
+
//#endregion
|
|
221
|
+
//#region src/tools/owner.ts
|
|
222
|
+
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");
|
|
223
|
+
async function _(e, t) {
|
|
224
|
+
let n = t?.trim();
|
|
225
|
+
if (!n) return;
|
|
226
|
+
if (n.toLowerCase() !== "me") return n;
|
|
227
|
+
let r = await e.api.request("GET", "/api/v1/me");
|
|
228
|
+
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.");
|
|
229
|
+
return r.email;
|
|
185
230
|
}
|
|
186
231
|
//#endregion
|
|
187
232
|
//#region src/tools/flags.ts
|
|
188
|
-
var
|
|
233
|
+
var v = t.enum([
|
|
189
234
|
"Boolean",
|
|
190
235
|
"String",
|
|
191
236
|
"Number",
|
|
192
237
|
"Json"
|
|
193
238
|
]);
|
|
194
|
-
function
|
|
239
|
+
function y(e, n) {
|
|
195
240
|
let r = (e) => `/api/v1/orgs/${i(n.org)}/projects/${i(e)}/flags`;
|
|
196
241
|
e.registerTool("list_flags", {
|
|
197
242
|
title: "List feature flags",
|
|
198
|
-
description: "List feature flags in a project. Filter with search (key/name substring), tag, type, archived. Paginated via cursor.",
|
|
243
|
+
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.",
|
|
199
244
|
inputSchema: t.object({
|
|
200
245
|
project: t.string().describe("Project key"),
|
|
201
246
|
search: t.string().optional(),
|
|
202
247
|
tag: t.string().optional(),
|
|
203
|
-
type:
|
|
248
|
+
type: v.optional(),
|
|
204
249
|
archived: t.boolean().optional().describe("true = only archived, false = only active"),
|
|
250
|
+
owner: g,
|
|
205
251
|
limit: t.number().int().min(1).max(100).optional(),
|
|
206
252
|
cursor: t.string().optional()
|
|
207
253
|
}),
|
|
208
254
|
annotations: { readOnlyHint: !0 }
|
|
209
|
-
}, async ({ project: e,
|
|
255
|
+
}, async ({ project: e, owner: t, ...i }) => d(async () => l(await n.api.request("GET", r(e), { query: {
|
|
256
|
+
...i,
|
|
257
|
+
owner: await _(n, t)
|
|
258
|
+
} })))), e.registerTool("get_flag", {
|
|
210
259
|
title: "Get feature flag",
|
|
211
|
-
description: "Get one feature flag with its variations and metadata. Address by flag key or id.",
|
|
260
|
+
description: "Get one feature flag with its variations and metadata, including its owner (null when unowned). Address by flag key or id.",
|
|
212
261
|
inputSchema: t.object({
|
|
213
262
|
project: t.string().describe("Project key"),
|
|
214
263
|
flag: t.string().describe("Flag key or id")
|
|
215
264
|
}),
|
|
216
265
|
annotations: { readOnlyHint: !0 }
|
|
217
|
-
}, async ({ project: e, flag: t }) =>
|
|
266
|
+
}, async ({ project: e, flag: t }) => d(async () => l(await n.api.request("GET", `${r(e)}/${i(t)}`)))), e.registerTool("create_flag", {
|
|
218
267
|
title: "Create feature flag",
|
|
219
268
|
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
269
|
inputSchema: t.object({
|
|
221
270
|
project: t.string().describe("Project key"),
|
|
222
271
|
key: t.string().describe("Unique flag key, e.g. checkout-v2"),
|
|
223
272
|
name: t.string(),
|
|
224
|
-
type:
|
|
273
|
+
type: v,
|
|
225
274
|
description: t.string().optional(),
|
|
226
275
|
tags: t.array(t.string()).optional(),
|
|
227
276
|
clientSideVisible: t.boolean().optional().describe("Expose to client-side SDKs (browser/mobile)"),
|
|
@@ -231,15 +280,17 @@ function m(e, n) {
|
|
|
231
280
|
value: t.string().describe("Variation value serialized as a string"),
|
|
232
281
|
description: t.string().optional()
|
|
233
282
|
})).optional(),
|
|
283
|
+
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)"),
|
|
284
|
+
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"),
|
|
234
285
|
idempotency_key: t.string().optional().describe("Idempotency-Key header for safe retries")
|
|
235
286
|
}),
|
|
236
287
|
annotations: { destructiveHint: !1 }
|
|
237
|
-
}, async ({ project: e, idempotency_key: t, ...i }) =>
|
|
288
|
+
}, async ({ project: e, idempotency_key: t, ...i }) => d(async () => l(await n.api.request("POST", r(e), {
|
|
238
289
|
body: i,
|
|
239
290
|
idempotencyKey: t
|
|
240
291
|
})))), e.registerTool("update_flag", {
|
|
241
292
|
title: "Update feature flag metadata",
|
|
242
|
-
description: "Update flag name/description/tags/clientSideVisible. Key and type are immutable. Use toggle_flag / update_targeting for behavior changes.",
|
|
293
|
+
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.",
|
|
243
294
|
inputSchema: t.object({
|
|
244
295
|
project: t.string(),
|
|
245
296
|
flag: t.string().describe("Flag key or id"),
|
|
@@ -249,11 +300,57 @@ function m(e, n) {
|
|
|
249
300
|
clientSideVisible: t.boolean().optional()
|
|
250
301
|
}),
|
|
251
302
|
annotations: { destructiveHint: !0 }
|
|
252
|
-
}, async ({ project: e, flag: t,
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
303
|
+
}, async ({ project: e, flag: t, name: a, description: o, tags: s, clientSideVisible: c }) => d(async () => {
|
|
304
|
+
let u = `${r(e)}/${i(t)}`, d = a === void 0 || o === void 0 ? await n.api.request("GET", u) : void 0, f = {
|
|
305
|
+
name: a ?? d?.name,
|
|
306
|
+
description: o ?? d?.description,
|
|
307
|
+
tags: s,
|
|
308
|
+
clientSideVisible: c
|
|
309
|
+
};
|
|
310
|
+
return await n.api.request("PUT", u, { body: f }), l({
|
|
311
|
+
project: e,
|
|
312
|
+
flag: t,
|
|
313
|
+
updated: !0
|
|
314
|
+
});
|
|
315
|
+
})), e.registerTool("set_flag_expiry", {
|
|
316
|
+
title: "Set or clear flag expiry",
|
|
317
|
+
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), and with FEATURE_NOT_ENABLED while flag expiration is not enabled for the organization. Clearing is always allowed.",
|
|
318
|
+
inputSchema: t.object({
|
|
319
|
+
project: t.string(),
|
|
320
|
+
flag: t.string().describe("Flag key or id"),
|
|
321
|
+
expiresAtUtc: t.string().nullable().describe("Date (YYYY-MM-DD, end of that UTC day) or ISO-8601 timestamp, or null to clear the expiry")
|
|
322
|
+
}),
|
|
323
|
+
annotations: {
|
|
324
|
+
destructiveHint: !1,
|
|
325
|
+
idempotentHint: !0
|
|
326
|
+
}
|
|
327
|
+
}, async ({ project: e, flag: t, expiresAtUtc: a }) => d(async () => {
|
|
328
|
+
let o = `${r(e)}/${i(t)}/expiry`;
|
|
329
|
+
return a === null ? await n.api.request("DELETE", o) : await n.api.request("PUT", o, { body: { expiresAtUtc: a } }), l({
|
|
330
|
+
project: e,
|
|
331
|
+
flag: t,
|
|
332
|
+
expiresAtUtc: a
|
|
333
|
+
});
|
|
334
|
+
})), e.registerTool("set_flag_owner", {
|
|
335
|
+
title: "Set or clear flag owner",
|
|
336
|
+
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.",
|
|
337
|
+
inputSchema: t.object({
|
|
338
|
+
project: t.string(),
|
|
339
|
+
flag: t.string().describe("Flag key or id"),
|
|
340
|
+
owner: t.string().nullable().describe("Owner email, \"me\" for the token's own user, or null (or \"none\") to clear the owner")
|
|
341
|
+
}),
|
|
342
|
+
annotations: {
|
|
343
|
+
destructiveHint: !1,
|
|
344
|
+
idempotentHint: !0
|
|
345
|
+
}
|
|
346
|
+
}, async ({ project: e, flag: t, owner: a }) => d(async () => {
|
|
347
|
+
let o = `${r(e)}/${i(t)}/owner`, s = a?.trim().toLowerCase() === "none" ? void 0 : await _(n, a ?? void 0);
|
|
348
|
+
return s === void 0 ? await n.api.request("DELETE", o) : await n.api.request("PUT", o, { body: { email: s } }), l({
|
|
349
|
+
project: e,
|
|
350
|
+
flag: t,
|
|
351
|
+
owner: s ?? null
|
|
352
|
+
});
|
|
353
|
+
})), e.registerTool("delete_flag", {
|
|
257
354
|
title: "Delete feature flag",
|
|
258
355
|
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
356
|
inputSchema: t.object({
|
|
@@ -261,7 +358,7 @@ function m(e, n) {
|
|
|
261
358
|
flag: t.string().describe("Flag key or id")
|
|
262
359
|
}),
|
|
263
360
|
annotations: { destructiveHint: !0 }
|
|
264
|
-
}, async ({ project: e, flag: t }) =>
|
|
361
|
+
}, async ({ project: e, flag: t }) => d(async () => (await n.api.request("DELETE", `${r(e)}/${i(t)}`), l({ deleted: t })))), e.registerTool("archive_flag", {
|
|
265
362
|
title: "Archive feature flag",
|
|
266
363
|
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
364
|
inputSchema: t.object({
|
|
@@ -270,7 +367,7 @@ function m(e, n) {
|
|
|
270
367
|
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.")
|
|
271
368
|
}),
|
|
272
369
|
annotations: { destructiveHint: !0 }
|
|
273
|
-
}, async ({ project: e, flag: t, force: a }) =>
|
|
370
|
+
}, async ({ project: e, flag: t, force: a }) => d(async () => (await n.api.request("POST", `${r(e)}/${i(t)}/archive`, { query: a ? { force: !0 } : {} }), l({
|
|
274
371
|
flag: t,
|
|
275
372
|
archived: !0
|
|
276
373
|
})))), e.registerTool("restore_flag", {
|
|
@@ -281,19 +378,19 @@ function m(e, n) {
|
|
|
281
378
|
flag: t.string()
|
|
282
379
|
}),
|
|
283
380
|
annotations: { destructiveHint: !1 }
|
|
284
|
-
}, async ({ project: e, flag: t }) =>
|
|
381
|
+
}, async ({ project: e, flag: t }) => d(async () => (await n.api.request("POST", `${r(e)}/${i(t)}/restore`), l({
|
|
285
382
|
flag: t,
|
|
286
383
|
archived: !1
|
|
287
384
|
}))));
|
|
288
385
|
}
|
|
289
386
|
//#endregion
|
|
290
387
|
//#region src/tools/flag-environments.ts
|
|
291
|
-
var
|
|
388
|
+
var b = t.enum([
|
|
292
389
|
"SingleVariation",
|
|
293
390
|
"PercentageRollout",
|
|
294
391
|
"TargetedRollout"
|
|
295
392
|
]);
|
|
296
|
-
function
|
|
393
|
+
function x(e, n) {
|
|
297
394
|
let r = (e, t) => `/api/v1/orgs/${i(n.org)}/projects/${i(e)}/flags/${i(t)}`;
|
|
298
395
|
e.registerTool("toggle_flag", {
|
|
299
396
|
title: "Toggle flag in environment",
|
|
@@ -305,11 +402,11 @@ function g(e, n) {
|
|
|
305
402
|
enabled: t.boolean()
|
|
306
403
|
}),
|
|
307
404
|
annotations: { destructiveHint: !0 }
|
|
308
|
-
}, async ({ project: e, flag: t, environment: a, enabled:
|
|
405
|
+
}, 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({
|
|
309
406
|
project: e,
|
|
310
407
|
flag: t,
|
|
311
408
|
environment: a,
|
|
312
|
-
enabled:
|
|
409
|
+
enabled: o
|
|
313
410
|
})))), e.registerTool("update_flag_environment_config", {
|
|
314
411
|
title: "Update flag environment config",
|
|
315
412
|
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.",
|
|
@@ -318,18 +415,18 @@ function g(e, n) {
|
|
|
318
415
|
flag: t.string(),
|
|
319
416
|
environment: t.string(),
|
|
320
417
|
defaultVariationId: t.string().describe("Variation id to serve on fallthrough — required by the API"),
|
|
321
|
-
strategy:
|
|
418
|
+
strategy: b.optional(),
|
|
322
419
|
prerequisites: t.array(t.object({
|
|
323
420
|
prerequisiteFlagKey: t.string(),
|
|
324
421
|
expectedVariationKey: t.string()
|
|
325
422
|
})).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.")
|
|
326
423
|
}),
|
|
327
424
|
annotations: { destructiveHint: !0 }
|
|
328
|
-
}, async ({ project: e, flag: t, environment: a, ...
|
|
425
|
+
}, async ({ project: e, flag: t, environment: a, ...o }) => d(async () => (await n.api.request("PUT", `${r(e, t)}/environments/${i(a)}`, { body: o }), l({
|
|
329
426
|
project: e,
|
|
330
427
|
flag: t,
|
|
331
428
|
environment: a,
|
|
332
|
-
...
|
|
429
|
+
...o
|
|
333
430
|
})))), e.registerTool("flag_status", {
|
|
334
431
|
title: "Flag status across environments",
|
|
335
432
|
description: "Compact cross-environment view of one flag: enabled state, strategy, default variation, and prerequisites per environment.",
|
|
@@ -338,9 +435,9 @@ function g(e, n) {
|
|
|
338
435
|
flag: t.string()
|
|
339
436
|
}),
|
|
340
437
|
annotations: { readOnlyHint: !0 }
|
|
341
|
-
}, async ({ project: e, flag: t }) =>
|
|
342
|
-
let [i, a] = await Promise.all([n.api.request("GET", r(e, t)), n.api.request("GET", `${r(e, t)}/environments`)]),
|
|
343
|
-
return
|
|
438
|
+
}, async ({ project: e, flag: t }) => d(async () => {
|
|
439
|
+
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;
|
|
440
|
+
return l({
|
|
344
441
|
key: i.key ?? null,
|
|
345
442
|
name: i.name ?? null,
|
|
346
443
|
type: i.type ?? null,
|
|
@@ -350,7 +447,7 @@ function g(e, n) {
|
|
|
350
447
|
environment: e.environmentKey ?? null,
|
|
351
448
|
enabled: e.isEnabled ?? !1,
|
|
352
449
|
strategy: e.strategy ?? null,
|
|
353
|
-
defaultVariation:
|
|
450
|
+
defaultVariation: o(e.defaultVariationId),
|
|
354
451
|
prerequisites: e.prerequisites ?? []
|
|
355
452
|
}))
|
|
356
453
|
});
|
|
@@ -358,22 +455,22 @@ function g(e, n) {
|
|
|
358
455
|
}
|
|
359
456
|
//#endregion
|
|
360
457
|
//#region src/tools/targeting.ts
|
|
361
|
-
var
|
|
458
|
+
var S = t.object({
|
|
362
459
|
attribute: t.string().describe("Context attribute name, e.g. plan, country, user_id"),
|
|
363
460
|
operator: t.string().describe("OperatorType member name, case-sensitive — e.g. Equals, NotEquals, Contains, In, GreaterThan"),
|
|
364
461
|
values: t.array(t.string()),
|
|
365
462
|
negate: t.boolean().optional().default(!1)
|
|
366
|
-
}),
|
|
463
|
+
}), C = t.object({
|
|
367
464
|
description: t.string().optional(),
|
|
368
465
|
variationId: t.string().optional().describe("Variation served when the rule matches"),
|
|
369
466
|
rolloutPercentage: t.number().int().min(0).max(100).optional().describe("Percentage rollout for this rule"),
|
|
370
467
|
userSegmentId: t.string().optional().describe("Match users in this segment (see list_segments)"),
|
|
371
468
|
conditionGroups: t.array(t.object({
|
|
372
469
|
operator: t.enum(["And", "Or"]).describe("How conditions inside this group combine"),
|
|
373
|
-
conditions: t.array(
|
|
470
|
+
conditions: t.array(S)
|
|
374
471
|
})).optional().describe("Groups are ANDed together; conditions inside a group use the group operator")
|
|
375
472
|
});
|
|
376
|
-
function
|
|
473
|
+
function w(e, n) {
|
|
377
474
|
let r = (e, t, r) => `/api/v1/orgs/${i(n.org)}/projects/${i(e)}/flags/${i(t)}/environments/${i(r)}/targeting`;
|
|
378
475
|
e.registerTool("get_targeting", {
|
|
379
476
|
title: "Get targeting rules",
|
|
@@ -384,17 +481,17 @@ function y(e, n) {
|
|
|
384
481
|
environment: t.string()
|
|
385
482
|
}),
|
|
386
483
|
annotations: { readOnlyHint: !0 }
|
|
387
|
-
}, async ({ project: e, flag: t, environment: i }) =>
|
|
484
|
+
}, async ({ project: e, flag: t, environment: i }) => d(async () => l(await n.api.request("GET", r(e, t, i))))), e.registerTool("update_targeting", {
|
|
388
485
|
title: "Replace targeting rules",
|
|
389
486
|
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.",
|
|
390
487
|
inputSchema: t.object({
|
|
391
488
|
project: t.string(),
|
|
392
489
|
flag: t.string(),
|
|
393
490
|
environment: t.string(),
|
|
394
|
-
rules: t.array(
|
|
491
|
+
rules: t.array(C)
|
|
395
492
|
}),
|
|
396
493
|
annotations: { destructiveHint: !0 }
|
|
397
|
-
}, async ({ project: e, flag: t, environment: i, rules: a }) =>
|
|
494
|
+
}, async ({ project: e, flag: t, environment: i, rules: a }) => d(async () => (await n.api.request("PUT", r(e, t, i), { body: { rules: a } }), l({
|
|
398
495
|
project: e,
|
|
399
496
|
flag: t,
|
|
400
497
|
environment: i,
|
|
@@ -403,7 +500,7 @@ function y(e, n) {
|
|
|
403
500
|
}
|
|
404
501
|
//#endregion
|
|
405
502
|
//#region src/tools/variations.ts
|
|
406
|
-
function
|
|
503
|
+
function T(e, n) {
|
|
407
504
|
e.registerTool("manage_variation", {
|
|
408
505
|
title: "Add, update, or remove a variation",
|
|
409
506
|
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.",
|
|
@@ -422,68 +519,217 @@ function b(e, n) {
|
|
|
422
519
|
description: t.string().optional()
|
|
423
520
|
}),
|
|
424
521
|
annotations: { destructiveHint: !0 }
|
|
425
|
-
}, async ({ project: e, flag: t, action: r, variationId: a, key:
|
|
426
|
-
let
|
|
427
|
-
return r === "add" ? !
|
|
428
|
-
key:
|
|
429
|
-
name:
|
|
430
|
-
value:
|
|
522
|
+
}, async ({ project: e, flag: t, action: r, variationId: a, key: o, name: s, value: c, description: f }) => d(async () => {
|
|
523
|
+
let d = `/api/v1/orgs/${i(n.org)}/projects/${i(e)}/flags/${i(t)}/variations`;
|
|
524
|
+
return r === "add" ? !o || c === void 0 ? u(/* @__PURE__ */ Error("action=add requires key and value")) : l(await n.api.request("POST", d, { body: {
|
|
525
|
+
key: o,
|
|
526
|
+
name: s,
|
|
527
|
+
value: c,
|
|
431
528
|
description: f
|
|
432
|
-
} })) : a ? r === "update" ? (await n.api.request("PUT", `${
|
|
433
|
-
name:
|
|
434
|
-
value:
|
|
529
|
+
} })) : a ? r === "update" ? (await n.api.request("PUT", `${d}/${i(a)}`, { body: {
|
|
530
|
+
name: s,
|
|
531
|
+
value: c,
|
|
435
532
|
description: f
|
|
436
|
-
} }),
|
|
533
|
+
} }), 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)`));
|
|
437
534
|
}));
|
|
438
535
|
}
|
|
439
536
|
//#endregion
|
|
537
|
+
//#region src/tools/webhooks.ts
|
|
538
|
+
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 = {
|
|
539
|
+
limit: t.number().int().min(1).max(100).optional().describe("Page size (default 20)"),
|
|
540
|
+
cursor: t.string().optional().describe("next_cursor from a previous response")
|
|
541
|
+
};
|
|
542
|
+
function k(e, n) {
|
|
543
|
+
let r = `/api/v1/orgs/${i(n.org)}/webhooks`, a = (e) => `${r}/${i(e)}`;
|
|
544
|
+
async function o(e) {
|
|
545
|
+
let t = [];
|
|
546
|
+
for (let r of e) {
|
|
547
|
+
if (E.test(r)) {
|
|
548
|
+
t.push(r);
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
let e = await n.api.request("GET", `/api/v1/orgs/${i(n.org)}/projects/${i(r)}`);
|
|
552
|
+
if (!e?.id) throw Error(`Project "${r}" resolved without an id`);
|
|
553
|
+
t.push(e.id);
|
|
554
|
+
}
|
|
555
|
+
return t;
|
|
556
|
+
}
|
|
557
|
+
async function s(e) {
|
|
558
|
+
let t = e.map((e) => {
|
|
559
|
+
if (E.test(e)) return { id: e };
|
|
560
|
+
let t = e.indexOf("/"), n = e.slice(0, t), r = e.slice(t + 1);
|
|
561
|
+
if (t < 0 || !n || !r) throw Error(`Environment "${e}" needs its project: pass "<project>/<environment>", e.g. "web/production", or the environment's id.`);
|
|
562
|
+
return {
|
|
563
|
+
project: n,
|
|
564
|
+
env: r
|
|
565
|
+
};
|
|
566
|
+
}), r = [];
|
|
567
|
+
for (let e of t) {
|
|
568
|
+
if ("id" in e) {
|
|
569
|
+
r.push(e.id);
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
let t = await n.api.request("GET", `/api/v1/orgs/${i(n.org)}/projects/${i(e.project)}/environments/${i(e.env)}`);
|
|
573
|
+
if (!t?.id) throw Error(`Environment "${e.project}/${e.env}" resolved without an id`);
|
|
574
|
+
r.push(t.id);
|
|
575
|
+
}
|
|
576
|
+
return r;
|
|
577
|
+
}
|
|
578
|
+
e.registerTool("list_webhooks", {
|
|
579
|
+
title: "List webhook subscriptions",
|
|
580
|
+
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,
|
|
581
|
+
inputSchema: t.object({ ...O }),
|
|
582
|
+
annotations: { readOnlyHint: !0 }
|
|
583
|
+
}, async ({ limit: e, cursor: t }) => d(async () => {
|
|
584
|
+
let [i, a] = await Promise.all([n.api.request("GET", r, { query: {
|
|
585
|
+
limit: e,
|
|
586
|
+
cursor: t
|
|
587
|
+
} }), n.api.request("GET", `${r}/event-types`)]);
|
|
588
|
+
return l({
|
|
589
|
+
...i,
|
|
590
|
+
availableEventTypes: a
|
|
591
|
+
});
|
|
592
|
+
})), e.registerTool("list_webhook_deliveries", {
|
|
593
|
+
title: "List webhook deliveries",
|
|
594
|
+
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,
|
|
595
|
+
inputSchema: t.object({
|
|
596
|
+
id: t.string().describe("Subscription id (from list_webhooks)"),
|
|
597
|
+
...O
|
|
598
|
+
}),
|
|
599
|
+
annotations: { readOnlyHint: !0 }
|
|
600
|
+
}, async ({ id: e, limit: t, cursor: r }) => d(async () => l(await n.api.request("GET", `${a(e)}/deliveries`, { query: {
|
|
601
|
+
limit: t,
|
|
602
|
+
cursor: r
|
|
603
|
+
} })))), e.registerTool("manage_webhook", {
|
|
604
|
+
title: "Create, update, or delete a webhook subscription, or rotate its secret",
|
|
605
|
+
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,
|
|
606
|
+
inputSchema: t.object({
|
|
607
|
+
action: t.enum([
|
|
608
|
+
"create",
|
|
609
|
+
"update",
|
|
610
|
+
"delete",
|
|
611
|
+
"rotate_secret",
|
|
612
|
+
"retire_secret"
|
|
613
|
+
]),
|
|
614
|
+
id: t.string().optional().describe("Subscription id; required for everything except create"),
|
|
615
|
+
name: t.string().optional().describe("Required for create"),
|
|
616
|
+
url: t.string().optional().describe("Receiver URL; required for create"),
|
|
617
|
+
eventTypes: t.array(t.string()).optional().describe("Event types to send; empty or omitted = all"),
|
|
618
|
+
projects: t.array(t.string()).optional().describe("Project keys or ids to cover; empty or omitted on create = all"),
|
|
619
|
+
environments: t.array(t.string()).optional().describe("\"<project>/<environment>\" keys or environment ids; empty or omitted on create = all"),
|
|
620
|
+
enabled: t.boolean().optional().describe("update only; subscriptions are created enabled"),
|
|
621
|
+
secretId: t.string().optional().describe("Required for retire_secret"),
|
|
622
|
+
idempotency_key: t.string().optional().describe("Idempotency-Key header for create / rotate_secret")
|
|
623
|
+
}),
|
|
624
|
+
annotations: { destructiveHint: !0 }
|
|
625
|
+
}, async ({ action: e, id: t, name: c, url: f, eventTypes: p, projects: m, environments: h, enabled: g, secretId: _, idempotency_key: v }) => d(async () => {
|
|
626
|
+
if (e === "create") {
|
|
627
|
+
if (!c || !f) return u(/* @__PURE__ */ Error("action=create requires name and url"));
|
|
628
|
+
let e = h && await s(h), t = m && await o(m);
|
|
629
|
+
return l(await n.api.request("POST", r, {
|
|
630
|
+
body: {
|
|
631
|
+
name: c,
|
|
632
|
+
url: f,
|
|
633
|
+
provider: "GenericHttp",
|
|
634
|
+
eventTypes: p,
|
|
635
|
+
projectIds: t,
|
|
636
|
+
environmentIds: e
|
|
637
|
+
},
|
|
638
|
+
idempotencyKey: v
|
|
639
|
+
}));
|
|
640
|
+
}
|
|
641
|
+
if (!t) return u(/* @__PURE__ */ Error(`action=${e} requires id (see list_webhooks)`));
|
|
642
|
+
if (e === "update") {
|
|
643
|
+
let e = h && await s(h), r = m && await o(m), i = await n.api.request("GET", a(t)), u = {
|
|
644
|
+
name: c ?? i.name,
|
|
645
|
+
url: f ?? i.url,
|
|
646
|
+
eventTypes: p ?? i.eventTypes,
|
|
647
|
+
projectIds: r ?? i.projectIds,
|
|
648
|
+
environmentIds: e ?? i.environmentIds,
|
|
649
|
+
isEnabled: g ?? i.isEnabled
|
|
650
|
+
};
|
|
651
|
+
return await n.api.request("PUT", a(t), { body: u }), l({
|
|
652
|
+
id: t,
|
|
653
|
+
updated: !0,
|
|
654
|
+
...u
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
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({
|
|
658
|
+
id: t,
|
|
659
|
+
retiredSecretId: _
|
|
660
|
+
})) : u(/* @__PURE__ */ Error("action=retire_secret requires secretId (see list_webhooks)"));
|
|
661
|
+
})), e.registerTool("deliver_webhook", {
|
|
662
|
+
title: "Send a test event or redeliver",
|
|
663
|
+
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,
|
|
664
|
+
inputSchema: t.object({
|
|
665
|
+
action: t.enum(["test", "redeliver"]),
|
|
666
|
+
id: t.string().describe("Subscription id (from list_webhooks)"),
|
|
667
|
+
deliveryId: t.string().optional().describe("Required for redeliver")
|
|
668
|
+
}),
|
|
669
|
+
annotations: {
|
|
670
|
+
destructiveHint: !1,
|
|
671
|
+
openWorldHint: !0
|
|
672
|
+
}
|
|
673
|
+
}, 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({
|
|
674
|
+
id: t,
|
|
675
|
+
redelivered: r
|
|
676
|
+
})) : u(/* @__PURE__ */ Error("action=redeliver requires deliveryId (see list_webhook_deliveries)"))));
|
|
677
|
+
}
|
|
678
|
+
//#endregion
|
|
440
679
|
//#region src/tools/workflows/stale-flags.ts
|
|
441
|
-
var
|
|
442
|
-
function
|
|
680
|
+
var A = 50;
|
|
681
|
+
function j(e, n) {
|
|
443
682
|
e.registerTool("find_stale_flags", {
|
|
444
683
|
title: "Find stale flags",
|
|
445
|
-
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 ${
|
|
684
|
+
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). Pass owner to see one person's stale flags ("me" for your own) or "none" for the unowned ones. Checks at most ${A} candidates per call, expired flags first.`,
|
|
446
685
|
inputSchema: t.object({
|
|
447
686
|
project: t.string(),
|
|
448
|
-
days: t.number().int().min(1).optional().default(30).describe("Minimum age in days since last update")
|
|
687
|
+
days: t.number().int().min(1).optional().default(30).describe("Minimum age in days since last update"),
|
|
688
|
+
owner: g
|
|
449
689
|
}),
|
|
450
690
|
annotations: { readOnlyHint: !0 }
|
|
451
|
-
}, async ({ project: e, days: t }) =>
|
|
452
|
-
let r = `/api/v1/orgs/${i(n.org)}/projects/${i(e)}/flags`,
|
|
691
|
+
}, async ({ project: e, days: t, owner: r }) => d(async () => {
|
|
692
|
+
let a = await _(n, r), o = `/api/v1/orgs/${i(n.org)}/projects/${i(e)}/flags`, s = Date.now(), c = s - t * 24 * 60 * 60 * 1e3, u = (e) => !!e.expiresAtUtc && new Date(e.expiresAtUtc).getTime() <= s, d = [], f;
|
|
453
693
|
do {
|
|
454
|
-
let e = await n.api.request("GET",
|
|
694
|
+
let e = await n.api.request("GET", o, { query: {
|
|
455
695
|
archived: !1,
|
|
456
696
|
limit: 100,
|
|
457
|
-
cursor:
|
|
697
|
+
cursor: f,
|
|
698
|
+
owner: a
|
|
458
699
|
} });
|
|
459
|
-
|
|
460
|
-
} while (
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
700
|
+
d.push(...(e.items ?? []).filter((e) => !!e.key && !!e.name && !!e.updatedAt && (new Date(e.updatedAt).getTime() < c || u(e)))), f = e.next_cursor ?? void 0;
|
|
701
|
+
} while (f);
|
|
702
|
+
d.sort((e, t) => Number(u(t)) - Number(u(e)));
|
|
703
|
+
let p = d.length > A, m = d.slice(0, A), h = [];
|
|
704
|
+
for (let e of m) {
|
|
705
|
+
let t = await n.api.request("GET", `${o}/${i(e.key)}/environments`);
|
|
464
706
|
if (t.length === 0) continue;
|
|
465
|
-
let a = t.every((e) => e.isEnabled),
|
|
466
|
-
(a ||
|
|
707
|
+
let r = u(e), a = t.every((e) => e.isEnabled), s = t.every((e) => !e.isEnabled);
|
|
708
|
+
(a || s || r) && h.push({
|
|
467
709
|
key: e.key,
|
|
468
710
|
name: e.name,
|
|
469
711
|
updatedAt: e.updatedAt,
|
|
470
|
-
|
|
712
|
+
expiresAtUtc: e.expiresAtUtc ?? null,
|
|
713
|
+
expired: r,
|
|
714
|
+
owner: e.owner ?? null,
|
|
715
|
+
reason: a ? "enabled-everywhere" : s ? "disabled-everywhere" : "past-expiry",
|
|
471
716
|
environments: t.length
|
|
472
717
|
});
|
|
473
718
|
}
|
|
474
|
-
return
|
|
719
|
+
return l({
|
|
475
720
|
project: e,
|
|
476
721
|
olderThanDays: t,
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
722
|
+
...a ? { owner: a } : {},
|
|
723
|
+
checked: m.length,
|
|
724
|
+
truncated: p,
|
|
725
|
+
...p ? { note: `Only the first ${A} of ${d.length} candidates were checked.` } : {},
|
|
726
|
+
stale: h
|
|
481
727
|
});
|
|
482
728
|
}));
|
|
483
729
|
}
|
|
484
730
|
//#endregion
|
|
485
731
|
//#region src/tools/workflows/snippets.ts
|
|
486
|
-
var
|
|
732
|
+
var M = [
|
|
487
733
|
"js",
|
|
488
734
|
"node",
|
|
489
735
|
"browser",
|
|
@@ -495,16 +741,16 @@ var C = [
|
|
|
495
741
|
"php",
|
|
496
742
|
"ruby",
|
|
497
743
|
"swift"
|
|
498
|
-
],
|
|
744
|
+
], N = [
|
|
499
745
|
"browser",
|
|
500
746
|
"react",
|
|
501
747
|
"swift"
|
|
502
748
|
];
|
|
503
|
-
function
|
|
504
|
-
return
|
|
749
|
+
function P(e) {
|
|
750
|
+
return N.includes(e);
|
|
505
751
|
}
|
|
506
|
-
var
|
|
507
|
-
function
|
|
752
|
+
var F = "Uses a SERVER-side SDK key (Featureflip dashboard → environment → SDK keys).", I = "Uses a CLIENT-side SDK key (safe to ship to browsers/apps).";
|
|
753
|
+
function L(e, t) {
|
|
508
754
|
switch (e) {
|
|
509
755
|
case "js": return {
|
|
510
756
|
install: "npm install @featureflip/js",
|
|
@@ -519,7 +765,7 @@ const enabled = client.boolVariation('${t}', { user_id: userId }, false);
|
|
|
519
765
|
if (enabled) {
|
|
520
766
|
// new code path
|
|
521
767
|
}`,
|
|
522
|
-
notes:
|
|
768
|
+
notes: F
|
|
523
769
|
};
|
|
524
770
|
case "node": return {
|
|
525
771
|
install: "npm install @featureflip/node",
|
|
@@ -530,7 +776,7 @@ const enabled = client.boolVariation('${t}', { user_id: userId }, false);
|
|
|
530
776
|
if (enabled) {
|
|
531
777
|
// new code path
|
|
532
778
|
}`,
|
|
533
|
-
notes:
|
|
779
|
+
notes: F
|
|
534
780
|
};
|
|
535
781
|
case "browser": return {
|
|
536
782
|
install: "npm install @featureflip/browser",
|
|
@@ -542,7 +788,7 @@ const enabled = client.boolVariation('${t}', false);
|
|
|
542
788
|
if (enabled) {
|
|
543
789
|
// new code path
|
|
544
790
|
}`,
|
|
545
|
-
notes:
|
|
791
|
+
notes: I
|
|
546
792
|
};
|
|
547
793
|
case "react": return {
|
|
548
794
|
install: "npm install @featureflip/react @featureflip/browser",
|
|
@@ -555,7 +801,7 @@ function MyComponent() {
|
|
|
555
801
|
const enabled = useFeatureFlag('${t}', false);
|
|
556
802
|
return enabled ? <NewExperience /> : <CurrentExperience />;
|
|
557
803
|
}`,
|
|
558
|
-
notes:
|
|
804
|
+
notes: I
|
|
559
805
|
};
|
|
560
806
|
case "python": return {
|
|
561
807
|
install: "pip install featureflip",
|
|
@@ -566,7 +812,7 @@ enabled = client.variation("${t}", {"user_id": user_id}, default=False)
|
|
|
566
812
|
if enabled:
|
|
567
813
|
# new code path
|
|
568
814
|
...`,
|
|
569
|
-
notes:
|
|
815
|
+
notes: F
|
|
570
816
|
};
|
|
571
817
|
case "csharp": return {
|
|
572
818
|
install: "dotnet add package Featureflip.Client",
|
|
@@ -579,7 +825,7 @@ if (enabled)
|
|
|
579
825
|
{
|
|
580
826
|
// new code path
|
|
581
827
|
}`,
|
|
582
|
-
notes:
|
|
828
|
+
notes: F
|
|
583
829
|
};
|
|
584
830
|
case "java": return {
|
|
585
831
|
install: "implementation 'io.featureflip:featureflip-java:2.0.0'",
|
|
@@ -593,7 +839,7 @@ boolean enabled = client.boolVariation("${t}",
|
|
|
593
839
|
if (enabled) {
|
|
594
840
|
// new code path
|
|
595
841
|
}`,
|
|
596
|
-
notes:
|
|
842
|
+
notes: F
|
|
597
843
|
};
|
|
598
844
|
case "go": return {
|
|
599
845
|
install: "go get github.com/canopy-labs/featureflip-go/v2",
|
|
@@ -609,7 +855,7 @@ ctx := featureflip.EvaluationContext{UserID: userID}
|
|
|
609
855
|
if client.BoolVariation("${t}", ctx, false) {
|
|
610
856
|
// new code path
|
|
611
857
|
}`,
|
|
612
|
-
notes:
|
|
858
|
+
notes: F
|
|
613
859
|
};
|
|
614
860
|
case "php": return {
|
|
615
861
|
install: "composer require featureflip/featureflip-php",
|
|
@@ -624,7 +870,7 @@ $enabled = $client->boolVariation('${t}', ['user_id' => $userId], false);
|
|
|
624
870
|
if ($enabled) {
|
|
625
871
|
// new code path
|
|
626
872
|
}`,
|
|
627
|
-
notes:
|
|
873
|
+
notes: F + " Requires PSR-16 cache and PSR-18 HTTP implementations."
|
|
628
874
|
};
|
|
629
875
|
case "ruby": return {
|
|
630
876
|
install: "gem \"featureflip\"",
|
|
@@ -635,7 +881,7 @@ enabled = client.bool_variation("${t}", { "user_id" => user_id }, false)
|
|
|
635
881
|
if enabled
|
|
636
882
|
# new code path
|
|
637
883
|
end`,
|
|
638
|
-
notes:
|
|
884
|
+
notes: F
|
|
639
885
|
};
|
|
640
886
|
case "swift": return {
|
|
641
887
|
install: ".package(url: \"https://github.com/canopy-labs/featureflip-swift\", from: \"2.0.0\")",
|
|
@@ -648,13 +894,13 @@ let enabled = client.boolVariation("${t}", default: false)
|
|
|
648
894
|
if enabled {
|
|
649
895
|
// new code path
|
|
650
896
|
}`,
|
|
651
|
-
notes:
|
|
897
|
+
notes: I
|
|
652
898
|
};
|
|
653
899
|
}
|
|
654
900
|
}
|
|
655
901
|
//#endregion
|
|
656
902
|
//#region src/tools/workflows/wrap-feature.ts
|
|
657
|
-
function
|
|
903
|
+
function R(e, n) {
|
|
658
904
|
e.registerTool("wrap_feature", {
|
|
659
905
|
title: "Wrap a feature in a new flag",
|
|
660
906
|
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.",
|
|
@@ -664,26 +910,26 @@ function k(e, n) {
|
|
|
664
910
|
name: t.string().optional().describe("Display name; defaults to the key"),
|
|
665
911
|
description: t.string().optional().describe("What this flag guards"),
|
|
666
912
|
tags: t.array(t.string()).optional(),
|
|
667
|
-
language: t.enum(
|
|
913
|
+
language: t.enum(M).describe("SDK language of the codebase being edited"),
|
|
668
914
|
idempotency_key: t.string().optional()
|
|
669
915
|
}),
|
|
670
916
|
annotations: { destructiveHint: !1 }
|
|
671
|
-
}, async ({ project: e, key: t, name: r, description: a, tags:
|
|
672
|
-
let
|
|
917
|
+
}, async ({ project: e, key: t, name: r, description: a, tags: o, language: s, idempotency_key: c }) => d(async () => {
|
|
918
|
+
let u = await n.api.request("POST", `/api/v1/orgs/${i(n.org)}/projects/${i(e)}/flags`, {
|
|
673
919
|
body: {
|
|
674
920
|
key: t,
|
|
675
921
|
name: r ?? t,
|
|
676
922
|
type: "Boolean",
|
|
677
923
|
description: a,
|
|
678
|
-
tags:
|
|
679
|
-
...
|
|
924
|
+
tags: o,
|
|
925
|
+
...P(s) ? { clientSideVisible: !0 } : {}
|
|
680
926
|
},
|
|
681
|
-
idempotencyKey:
|
|
682
|
-
}), d =
|
|
683
|
-
return
|
|
684
|
-
flag:
|
|
927
|
+
idempotencyKey: c
|
|
928
|
+
}), d = L(s, t);
|
|
929
|
+
return l({
|
|
930
|
+
flag: u,
|
|
685
931
|
integration: {
|
|
686
|
-
language:
|
|
932
|
+
language: s,
|
|
687
933
|
install: d.install,
|
|
688
934
|
code: d.code,
|
|
689
935
|
notes: d.notes
|
|
@@ -698,17 +944,17 @@ function k(e, n) {
|
|
|
698
944
|
}
|
|
699
945
|
//#endregion
|
|
700
946
|
//#region src/server.ts
|
|
701
|
-
function
|
|
947
|
+
function z(t) {
|
|
702
948
|
let r = new e({
|
|
703
949
|
name: "featureflip",
|
|
704
950
|
version: n
|
|
705
951
|
});
|
|
706
|
-
return
|
|
952
|
+
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), j(r, t), R(r, t), r;
|
|
707
953
|
}
|
|
708
954
|
//#endregion
|
|
709
955
|
//#region src/config.ts
|
|
710
|
-
var
|
|
711
|
-
function
|
|
956
|
+
var B = "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.";
|
|
957
|
+
function V(e = process.env) {
|
|
712
958
|
let t = e.FEATUREFLIP_TOKEN?.trim();
|
|
713
959
|
return t ? {
|
|
714
960
|
token: t,
|
|
@@ -716,14 +962,14 @@ function M(e = process.env) {
|
|
|
716
962
|
org: e.FEATUREFLIP_ORG?.trim() || void 0
|
|
717
963
|
} : null;
|
|
718
964
|
}
|
|
719
|
-
function
|
|
720
|
-
let t =
|
|
721
|
-
if (!t) throw Error(
|
|
965
|
+
function H(e = process.env) {
|
|
966
|
+
let t = V(e);
|
|
967
|
+
if (!t) throw Error(B);
|
|
722
968
|
return t;
|
|
723
969
|
}
|
|
724
970
|
//#endregion
|
|
725
971
|
//#region src/org.ts
|
|
726
|
-
async function
|
|
972
|
+
async function U(e, t) {
|
|
727
973
|
if (t) return t;
|
|
728
974
|
let n = (await e.request("GET", "/api/v1/orgs", { query: { limit: 100 } })).items ?? [];
|
|
729
975
|
if (n.length === 1) {
|
|
@@ -734,4 +980,4 @@ async function P(e, t) {
|
|
|
734
980
|
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(", "));
|
|
735
981
|
}
|
|
736
982
|
//#endregion
|
|
737
|
-
export {
|
|
983
|
+
export { z as a, n as c, V as i, B as n, r as o, H as r, c as s, U as t };
|