@featureflip/mcp 0.1.8 → 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 +3 -2
- package/dist/cli.mjs +1 -1
- package/dist/generated/api-types.d.ts +229 -12
- package/dist/generated/api-types.d.ts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{org-DZ1ge1Mx.js → org-8tn1Su68.js} +138 -91
- 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/workflows/stale-flags.d.ts.map +1 -1
- package/package.json +4 -4
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as e, c as t, o as n, r, s as i, t as a } from "./org-
|
|
1
|
+
import { a as e, c as t, o as n, r, s as i, t as a } from "./org-8tn1Su68.js";
|
|
2
2
|
export { n as ApiError, i as FeatureflipApi, t as PKG_VERSION, e as createServer, r as loadConfig, a as resolveOrg };
|
|
@@ -4,7 +4,7 @@ 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
10
|
keywords: [
|
|
@@ -43,15 +43,15 @@ var n = {
|
|
|
43
43
|
"smoke:docker": "node scripts/docker-smoke.mjs"
|
|
44
44
|
},
|
|
45
45
|
dependencies: {
|
|
46
|
-
"@modelcontextprotocol/server": "2.
|
|
46
|
+
"@modelcontextprotocol/server": "2.1.0",
|
|
47
47
|
zod: "^4.6.5"
|
|
48
48
|
},
|
|
49
49
|
devDependencies: {
|
|
50
|
-
"@modelcontextprotocol/client": "2.
|
|
50
|
+
"@modelcontextprotocol/client": "2.1.0",
|
|
51
51
|
"@typescript/typescript6": "^6.0.2",
|
|
52
52
|
"@types/node": "^26.6.2",
|
|
53
53
|
typescript: "^7.0.2",
|
|
54
|
-
vite: "^8.3.
|
|
54
|
+
vite: "^8.3.1",
|
|
55
55
|
"vite-plugin-dts": "^5.1.1",
|
|
56
56
|
vitest: "^5.0.1"
|
|
57
57
|
},
|
|
@@ -218,31 +218,46 @@ function h(e, n) {
|
|
|
218
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
219
|
}
|
|
220
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;
|
|
230
|
+
}
|
|
231
|
+
//#endregion
|
|
221
232
|
//#region src/tools/flags.ts
|
|
222
|
-
var
|
|
233
|
+
var v = t.enum([
|
|
223
234
|
"Boolean",
|
|
224
235
|
"String",
|
|
225
236
|
"Number",
|
|
226
237
|
"Json"
|
|
227
238
|
]);
|
|
228
|
-
function
|
|
239
|
+
function y(e, n) {
|
|
229
240
|
let r = (e) => `/api/v1/orgs/${i(n.org)}/projects/${i(e)}/flags`;
|
|
230
241
|
e.registerTool("list_flags", {
|
|
231
242
|
title: "List feature flags",
|
|
232
|
-
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.",
|
|
233
244
|
inputSchema: t.object({
|
|
234
245
|
project: t.string().describe("Project key"),
|
|
235
246
|
search: t.string().optional(),
|
|
236
247
|
tag: t.string().optional(),
|
|
237
|
-
type:
|
|
248
|
+
type: v.optional(),
|
|
238
249
|
archived: t.boolean().optional().describe("true = only archived, false = only active"),
|
|
250
|
+
owner: g,
|
|
239
251
|
limit: t.number().int().min(1).max(100).optional(),
|
|
240
252
|
cursor: t.string().optional()
|
|
241
253
|
}),
|
|
242
254
|
annotations: { readOnlyHint: !0 }
|
|
243
|
-
}, 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", {
|
|
244
259
|
title: "Get feature flag",
|
|
245
|
-
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.",
|
|
246
261
|
inputSchema: t.object({
|
|
247
262
|
project: t.string().describe("Project key"),
|
|
248
263
|
flag: t.string().describe("Flag key or id")
|
|
@@ -255,7 +270,7 @@ function _(e, n) {
|
|
|
255
270
|
project: t.string().describe("Project key"),
|
|
256
271
|
key: t.string().describe("Unique flag key, e.g. checkout-v2"),
|
|
257
272
|
name: t.string(),
|
|
258
|
-
type:
|
|
273
|
+
type: v,
|
|
259
274
|
description: t.string().optional(),
|
|
260
275
|
tags: t.array(t.string()).optional(),
|
|
261
276
|
clientSideVisible: t.boolean().optional().describe("Expose to client-side SDKs (browser/mobile)"),
|
|
@@ -265,7 +280,8 @@ function _(e, n) {
|
|
|
265
280
|
value: t.string().describe("Variation value serialized as a string"),
|
|
266
281
|
description: t.string().optional()
|
|
267
282
|
})).optional(),
|
|
268
|
-
expiresAtUtc: t.string().optional().describe("Optional ISO-8601
|
|
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"),
|
|
269
285
|
idempotency_key: t.string().optional().describe("Idempotency-Key header for safe retries")
|
|
270
286
|
}),
|
|
271
287
|
annotations: { destructiveHint: !1 }
|
|
@@ -274,7 +290,7 @@ function _(e, n) {
|
|
|
274
290
|
idempotencyKey: t
|
|
275
291
|
})))), e.registerTool("update_flag", {
|
|
276
292
|
title: "Update feature flag metadata",
|
|
277
|
-
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.",
|
|
278
294
|
inputSchema: t.object({
|
|
279
295
|
project: t.string(),
|
|
280
296
|
flag: t.string().describe("Flag key or id"),
|
|
@@ -284,17 +300,25 @@ function _(e, n) {
|
|
|
284
300
|
clientSideVisible: t.boolean().optional()
|
|
285
301
|
}),
|
|
286
302
|
annotations: { destructiveHint: !0 }
|
|
287
|
-
}, async ({ project: e, flag: t,
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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", {
|
|
292
316
|
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
|
|
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.",
|
|
294
318
|
inputSchema: t.object({
|
|
295
319
|
project: t.string(),
|
|
296
320
|
flag: t.string().describe("Flag key or id"),
|
|
297
|
-
expiresAtUtc: t.string().nullable().describe("ISO-8601
|
|
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")
|
|
298
322
|
}),
|
|
299
323
|
annotations: {
|
|
300
324
|
destructiveHint: !1,
|
|
@@ -307,6 +331,25 @@ function _(e, n) {
|
|
|
307
331
|
flag: t,
|
|
308
332
|
expiresAtUtc: a
|
|
309
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
|
+
});
|
|
310
353
|
})), e.registerTool("delete_flag", {
|
|
311
354
|
title: "Delete feature flag",
|
|
312
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.",
|
|
@@ -342,12 +385,12 @@ function _(e, n) {
|
|
|
342
385
|
}
|
|
343
386
|
//#endregion
|
|
344
387
|
//#region src/tools/flag-environments.ts
|
|
345
|
-
var
|
|
388
|
+
var b = t.enum([
|
|
346
389
|
"SingleVariation",
|
|
347
390
|
"PercentageRollout",
|
|
348
391
|
"TargetedRollout"
|
|
349
392
|
]);
|
|
350
|
-
function
|
|
393
|
+
function x(e, n) {
|
|
351
394
|
let r = (e, t) => `/api/v1/orgs/${i(n.org)}/projects/${i(e)}/flags/${i(t)}`;
|
|
352
395
|
e.registerTool("toggle_flag", {
|
|
353
396
|
title: "Toggle flag in environment",
|
|
@@ -372,7 +415,7 @@ function y(e, n) {
|
|
|
372
415
|
flag: t.string(),
|
|
373
416
|
environment: t.string(),
|
|
374
417
|
defaultVariationId: t.string().describe("Variation id to serve on fallthrough — required by the API"),
|
|
375
|
-
strategy:
|
|
418
|
+
strategy: b.optional(),
|
|
376
419
|
prerequisites: t.array(t.object({
|
|
377
420
|
prerequisiteFlagKey: t.string(),
|
|
378
421
|
expectedVariationKey: t.string()
|
|
@@ -412,22 +455,22 @@ function y(e, n) {
|
|
|
412
455
|
}
|
|
413
456
|
//#endregion
|
|
414
457
|
//#region src/tools/targeting.ts
|
|
415
|
-
var
|
|
458
|
+
var S = t.object({
|
|
416
459
|
attribute: t.string().describe("Context attribute name, e.g. plan, country, user_id"),
|
|
417
460
|
operator: t.string().describe("OperatorType member name, case-sensitive — e.g. Equals, NotEquals, Contains, In, GreaterThan"),
|
|
418
461
|
values: t.array(t.string()),
|
|
419
462
|
negate: t.boolean().optional().default(!1)
|
|
420
|
-
}),
|
|
463
|
+
}), C = t.object({
|
|
421
464
|
description: t.string().optional(),
|
|
422
465
|
variationId: t.string().optional().describe("Variation served when the rule matches"),
|
|
423
466
|
rolloutPercentage: t.number().int().min(0).max(100).optional().describe("Percentage rollout for this rule"),
|
|
424
467
|
userSegmentId: t.string().optional().describe("Match users in this segment (see list_segments)"),
|
|
425
468
|
conditionGroups: t.array(t.object({
|
|
426
469
|
operator: t.enum(["And", "Or"]).describe("How conditions inside this group combine"),
|
|
427
|
-
conditions: t.array(
|
|
470
|
+
conditions: t.array(S)
|
|
428
471
|
})).optional().describe("Groups are ANDed together; conditions inside a group use the group operator")
|
|
429
472
|
});
|
|
430
|
-
function
|
|
473
|
+
function w(e, n) {
|
|
431
474
|
let r = (e, t, r) => `/api/v1/orgs/${i(n.org)}/projects/${i(e)}/flags/${i(t)}/environments/${i(r)}/targeting`;
|
|
432
475
|
e.registerTool("get_targeting", {
|
|
433
476
|
title: "Get targeting rules",
|
|
@@ -445,7 +488,7 @@ function S(e, n) {
|
|
|
445
488
|
project: t.string(),
|
|
446
489
|
flag: t.string(),
|
|
447
490
|
environment: t.string(),
|
|
448
|
-
rules: t.array(
|
|
491
|
+
rules: t.array(C)
|
|
449
492
|
}),
|
|
450
493
|
annotations: { destructiveHint: !0 }
|
|
451
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({
|
|
@@ -457,7 +500,7 @@ function S(e, n) {
|
|
|
457
500
|
}
|
|
458
501
|
//#endregion
|
|
459
502
|
//#region src/tools/variations.ts
|
|
460
|
-
function
|
|
503
|
+
function T(e, n) {
|
|
461
504
|
e.registerTool("manage_variation", {
|
|
462
505
|
title: "Add, update, or remove a variation",
|
|
463
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.",
|
|
@@ -492,16 +535,16 @@ function C(e, n) {
|
|
|
492
535
|
}
|
|
493
536
|
//#endregion
|
|
494
537
|
//#region src/tools/webhooks.ts
|
|
495
|
-
var
|
|
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 = {
|
|
496
539
|
limit: t.number().int().min(1).max(100).optional().describe("Page size (default 20)"),
|
|
497
540
|
cursor: t.string().optional().describe("next_cursor from a previous response")
|
|
498
541
|
};
|
|
499
|
-
function
|
|
542
|
+
function k(e, n) {
|
|
500
543
|
let r = `/api/v1/orgs/${i(n.org)}/webhooks`, a = (e) => `${r}/${i(e)}`;
|
|
501
544
|
async function o(e) {
|
|
502
545
|
let t = [];
|
|
503
546
|
for (let r of e) {
|
|
504
|
-
if (
|
|
547
|
+
if (E.test(r)) {
|
|
505
548
|
t.push(r);
|
|
506
549
|
continue;
|
|
507
550
|
}
|
|
@@ -513,7 +556,7 @@ function D(e, n) {
|
|
|
513
556
|
}
|
|
514
557
|
async function s(e) {
|
|
515
558
|
let t = e.map((e) => {
|
|
516
|
-
if (
|
|
559
|
+
if (E.test(e)) return { id: e };
|
|
517
560
|
let t = e.indexOf("/"), n = e.slice(0, t), r = e.slice(t + 1);
|
|
518
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.`);
|
|
519
562
|
return {
|
|
@@ -534,8 +577,8 @@ function D(e, n) {
|
|
|
534
577
|
}
|
|
535
578
|
e.registerTool("list_webhooks", {
|
|
536
579
|
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. " +
|
|
538
|
-
inputSchema: t.object({ ...
|
|
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 }),
|
|
539
582
|
annotations: { readOnlyHint: !0 }
|
|
540
583
|
}, async ({ limit: e, cursor: t }) => d(async () => {
|
|
541
584
|
let [i, a] = await Promise.all([n.api.request("GET", r, { query: {
|
|
@@ -548,10 +591,10 @@ function D(e, n) {
|
|
|
548
591
|
});
|
|
549
592
|
})), e.registerTool("list_webhook_deliveries", {
|
|
550
593
|
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. " +
|
|
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,
|
|
552
595
|
inputSchema: t.object({
|
|
553
596
|
id: t.string().describe("Subscription id (from list_webhooks)"),
|
|
554
|
-
...
|
|
597
|
+
...O
|
|
555
598
|
}),
|
|
556
599
|
annotations: { readOnlyHint: !0 }
|
|
557
600
|
}, async ({ id: e, limit: t, cursor: r }) => d(async () => l(await n.api.request("GET", `${a(e)}/deliveries`, { query: {
|
|
@@ -559,7 +602,7 @@ function D(e, n) {
|
|
|
559
602
|
cursor: r
|
|
560
603
|
} })))), e.registerTool("manage_webhook", {
|
|
561
604
|
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. " +
|
|
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,
|
|
563
606
|
inputSchema: t.object({
|
|
564
607
|
action: t.enum([
|
|
565
608
|
"create",
|
|
@@ -617,7 +660,7 @@ function D(e, n) {
|
|
|
617
660
|
})) : u(/* @__PURE__ */ Error("action=retire_secret requires secretId (see list_webhooks)"));
|
|
618
661
|
})), e.registerTool("deliver_webhook", {
|
|
619
662
|
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. " +
|
|
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,
|
|
621
664
|
inputSchema: t.object({
|
|
622
665
|
action: t.enum(["test", "redeliver"]),
|
|
623
666
|
id: t.string().describe("Subscription id (from list_webhooks)"),
|
|
@@ -634,55 +677,59 @@ function D(e, n) {
|
|
|
634
677
|
}
|
|
635
678
|
//#endregion
|
|
636
679
|
//#region src/tools/workflows/stale-flags.ts
|
|
637
|
-
var
|
|
638
|
-
function
|
|
680
|
+
var A = 50;
|
|
681
|
+
function j(e, n) {
|
|
639
682
|
e.registerTool("find_stale_flags", {
|
|
640
683
|
title: "Find stale flags",
|
|
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
|
|
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.`,
|
|
642
685
|
inputSchema: t.object({
|
|
643
686
|
project: t.string(),
|
|
644
|
-
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
|
|
645
689
|
}),
|
|
646
690
|
annotations: { readOnlyHint: !0 }
|
|
647
|
-
}, async ({ project: e, days: t }) => d(async () => {
|
|
648
|
-
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;
|
|
649
693
|
do {
|
|
650
|
-
let e = await n.api.request("GET",
|
|
694
|
+
let e = await n.api.request("GET", o, { query: {
|
|
651
695
|
archived: !1,
|
|
652
696
|
limit: 100,
|
|
653
|
-
cursor:
|
|
697
|
+
cursor: f,
|
|
698
|
+
owner: a
|
|
654
699
|
} });
|
|
655
|
-
|
|
656
|
-
} while (
|
|
657
|
-
|
|
658
|
-
let
|
|
659
|
-
for (let e of
|
|
660
|
-
let t = await n.api.request("GET", `${
|
|
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`);
|
|
661
706
|
if (t.length === 0) continue;
|
|
662
|
-
let
|
|
663
|
-
(
|
|
707
|
+
let r = u(e), a = t.every((e) => e.isEnabled), s = t.every((e) => !e.isEnabled);
|
|
708
|
+
(a || s || r) && h.push({
|
|
664
709
|
key: e.key,
|
|
665
710
|
name: e.name,
|
|
666
711
|
updatedAt: e.updatedAt,
|
|
667
712
|
expiresAtUtc: e.expiresAtUtc ?? null,
|
|
668
|
-
expired:
|
|
669
|
-
|
|
713
|
+
expired: r,
|
|
714
|
+
owner: e.owner ?? null,
|
|
715
|
+
reason: a ? "enabled-everywhere" : s ? "disabled-everywhere" : "past-expiry",
|
|
670
716
|
environments: t.length
|
|
671
717
|
});
|
|
672
718
|
}
|
|
673
719
|
return l({
|
|
674
720
|
project: e,
|
|
675
721
|
olderThanDays: t,
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
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
|
|
680
727
|
});
|
|
681
728
|
}));
|
|
682
729
|
}
|
|
683
730
|
//#endregion
|
|
684
731
|
//#region src/tools/workflows/snippets.ts
|
|
685
|
-
var
|
|
732
|
+
var M = [
|
|
686
733
|
"js",
|
|
687
734
|
"node",
|
|
688
735
|
"browser",
|
|
@@ -694,16 +741,16 @@ var A = [
|
|
|
694
741
|
"php",
|
|
695
742
|
"ruby",
|
|
696
743
|
"swift"
|
|
697
|
-
],
|
|
744
|
+
], N = [
|
|
698
745
|
"browser",
|
|
699
746
|
"react",
|
|
700
747
|
"swift"
|
|
701
748
|
];
|
|
702
|
-
function
|
|
703
|
-
return
|
|
749
|
+
function P(e) {
|
|
750
|
+
return N.includes(e);
|
|
704
751
|
}
|
|
705
|
-
var
|
|
706
|
-
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) {
|
|
707
754
|
switch (e) {
|
|
708
755
|
case "js": return {
|
|
709
756
|
install: "npm install @featureflip/js",
|
|
@@ -718,7 +765,7 @@ const enabled = client.boolVariation('${t}', { user_id: userId }, false);
|
|
|
718
765
|
if (enabled) {
|
|
719
766
|
// new code path
|
|
720
767
|
}`,
|
|
721
|
-
notes:
|
|
768
|
+
notes: F
|
|
722
769
|
};
|
|
723
770
|
case "node": return {
|
|
724
771
|
install: "npm install @featureflip/node",
|
|
@@ -729,7 +776,7 @@ const enabled = client.boolVariation('${t}', { user_id: userId }, false);
|
|
|
729
776
|
if (enabled) {
|
|
730
777
|
// new code path
|
|
731
778
|
}`,
|
|
732
|
-
notes:
|
|
779
|
+
notes: F
|
|
733
780
|
};
|
|
734
781
|
case "browser": return {
|
|
735
782
|
install: "npm install @featureflip/browser",
|
|
@@ -741,7 +788,7 @@ const enabled = client.boolVariation('${t}', false);
|
|
|
741
788
|
if (enabled) {
|
|
742
789
|
// new code path
|
|
743
790
|
}`,
|
|
744
|
-
notes:
|
|
791
|
+
notes: I
|
|
745
792
|
};
|
|
746
793
|
case "react": return {
|
|
747
794
|
install: "npm install @featureflip/react @featureflip/browser",
|
|
@@ -754,7 +801,7 @@ function MyComponent() {
|
|
|
754
801
|
const enabled = useFeatureFlag('${t}', false);
|
|
755
802
|
return enabled ? <NewExperience /> : <CurrentExperience />;
|
|
756
803
|
}`,
|
|
757
|
-
notes:
|
|
804
|
+
notes: I
|
|
758
805
|
};
|
|
759
806
|
case "python": return {
|
|
760
807
|
install: "pip install featureflip",
|
|
@@ -765,7 +812,7 @@ enabled = client.variation("${t}", {"user_id": user_id}, default=False)
|
|
|
765
812
|
if enabled:
|
|
766
813
|
# new code path
|
|
767
814
|
...`,
|
|
768
|
-
notes:
|
|
815
|
+
notes: F
|
|
769
816
|
};
|
|
770
817
|
case "csharp": return {
|
|
771
818
|
install: "dotnet add package Featureflip.Client",
|
|
@@ -778,7 +825,7 @@ if (enabled)
|
|
|
778
825
|
{
|
|
779
826
|
// new code path
|
|
780
827
|
}`,
|
|
781
|
-
notes:
|
|
828
|
+
notes: F
|
|
782
829
|
};
|
|
783
830
|
case "java": return {
|
|
784
831
|
install: "implementation 'io.featureflip:featureflip-java:2.0.0'",
|
|
@@ -792,7 +839,7 @@ boolean enabled = client.boolVariation("${t}",
|
|
|
792
839
|
if (enabled) {
|
|
793
840
|
// new code path
|
|
794
841
|
}`,
|
|
795
|
-
notes:
|
|
842
|
+
notes: F
|
|
796
843
|
};
|
|
797
844
|
case "go": return {
|
|
798
845
|
install: "go get github.com/canopy-labs/featureflip-go/v2",
|
|
@@ -808,7 +855,7 @@ ctx := featureflip.EvaluationContext{UserID: userID}
|
|
|
808
855
|
if client.BoolVariation("${t}", ctx, false) {
|
|
809
856
|
// new code path
|
|
810
857
|
}`,
|
|
811
|
-
notes:
|
|
858
|
+
notes: F
|
|
812
859
|
};
|
|
813
860
|
case "php": return {
|
|
814
861
|
install: "composer require featureflip/featureflip-php",
|
|
@@ -823,7 +870,7 @@ $enabled = $client->boolVariation('${t}', ['user_id' => $userId], false);
|
|
|
823
870
|
if ($enabled) {
|
|
824
871
|
// new code path
|
|
825
872
|
}`,
|
|
826
|
-
notes:
|
|
873
|
+
notes: F + " Requires PSR-16 cache and PSR-18 HTTP implementations."
|
|
827
874
|
};
|
|
828
875
|
case "ruby": return {
|
|
829
876
|
install: "gem \"featureflip\"",
|
|
@@ -834,7 +881,7 @@ enabled = client.bool_variation("${t}", { "user_id" => user_id }, false)
|
|
|
834
881
|
if enabled
|
|
835
882
|
# new code path
|
|
836
883
|
end`,
|
|
837
|
-
notes:
|
|
884
|
+
notes: F
|
|
838
885
|
};
|
|
839
886
|
case "swift": return {
|
|
840
887
|
install: ".package(url: \"https://github.com/canopy-labs/featureflip-swift\", from: \"2.0.0\")",
|
|
@@ -847,13 +894,13 @@ let enabled = client.boolVariation("${t}", default: false)
|
|
|
847
894
|
if enabled {
|
|
848
895
|
// new code path
|
|
849
896
|
}`,
|
|
850
|
-
notes:
|
|
897
|
+
notes: I
|
|
851
898
|
};
|
|
852
899
|
}
|
|
853
900
|
}
|
|
854
901
|
//#endregion
|
|
855
902
|
//#region src/tools/workflows/wrap-feature.ts
|
|
856
|
-
function
|
|
903
|
+
function R(e, n) {
|
|
857
904
|
e.registerTool("wrap_feature", {
|
|
858
905
|
title: "Wrap a feature in a new flag",
|
|
859
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.",
|
|
@@ -863,7 +910,7 @@ function I(e, n) {
|
|
|
863
910
|
name: t.string().optional().describe("Display name; defaults to the key"),
|
|
864
911
|
description: t.string().optional().describe("What this flag guards"),
|
|
865
912
|
tags: t.array(t.string()).optional(),
|
|
866
|
-
language: t.enum(
|
|
913
|
+
language: t.enum(M).describe("SDK language of the codebase being edited"),
|
|
867
914
|
idempotency_key: t.string().optional()
|
|
868
915
|
}),
|
|
869
916
|
annotations: { destructiveHint: !1 }
|
|
@@ -875,10 +922,10 @@ function I(e, n) {
|
|
|
875
922
|
type: "Boolean",
|
|
876
923
|
description: a,
|
|
877
924
|
tags: o,
|
|
878
|
-
...
|
|
925
|
+
...P(s) ? { clientSideVisible: !0 } : {}
|
|
879
926
|
},
|
|
880
927
|
idempotencyKey: c
|
|
881
|
-
}), d =
|
|
928
|
+
}), d = L(s, t);
|
|
882
929
|
return l({
|
|
883
930
|
flag: u,
|
|
884
931
|
integration: {
|
|
@@ -897,17 +944,17 @@ function I(e, n) {
|
|
|
897
944
|
}
|
|
898
945
|
//#endregion
|
|
899
946
|
//#region src/server.ts
|
|
900
|
-
function
|
|
947
|
+
function z(t) {
|
|
901
948
|
let r = new e({
|
|
902
949
|
name: "featureflip",
|
|
903
950
|
version: n
|
|
904
951
|
});
|
|
905
|
-
return p(r, t), m(r, t), h(r, t),
|
|
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;
|
|
906
953
|
}
|
|
907
954
|
//#endregion
|
|
908
955
|
//#region src/config.ts
|
|
909
|
-
var
|
|
910
|
-
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) {
|
|
911
958
|
let t = e.FEATUREFLIP_TOKEN?.trim();
|
|
912
959
|
return t ? {
|
|
913
960
|
token: t,
|
|
@@ -915,14 +962,14 @@ function z(e = process.env) {
|
|
|
915
962
|
org: e.FEATUREFLIP_ORG?.trim() || void 0
|
|
916
963
|
} : null;
|
|
917
964
|
}
|
|
918
|
-
function
|
|
919
|
-
let t =
|
|
920
|
-
if (!t) throw Error(
|
|
965
|
+
function H(e = process.env) {
|
|
966
|
+
let t = V(e);
|
|
967
|
+
if (!t) throw Error(B);
|
|
921
968
|
return t;
|
|
922
969
|
}
|
|
923
970
|
//#endregion
|
|
924
971
|
//#region src/org.ts
|
|
925
|
-
async function
|
|
972
|
+
async function U(e, t) {
|
|
926
973
|
if (t) return t;
|
|
927
974
|
let n = (await e.request("GET", "/api/v1/orgs", { query: { limit: 100 } })).items ?? [];
|
|
928
975
|
if (n.length === 1) {
|
|
@@ -933,4 +980,4 @@ async function V(e, t) {
|
|
|
933
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(", "));
|
|
934
981
|
}
|
|
935
982
|
//#endregion
|
|
936
|
-
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 };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"flags.d.ts","sourceRoot":"","sources":["../../src/tools/flags.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAC;AAI9D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"flags.d.ts","sourceRoot":"","sources":["../../src/tools/flags.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAC;AAI9D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAKhD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,WAAW,GAAG,IAAI,CAgQ3E"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ToolContext } from './context.js';
|
|
2
|
+
import * as z from 'zod/v4';
|
|
3
|
+
export declare const ownerFilter: z.ZodOptional<z.ZodString>;
|
|
4
|
+
/**
|
|
5
|
+
* Turns the tool's `owner` argument into the /api/v1 `owner=` value, which takes only an email or
|
|
6
|
+
* `none`. `me` is looked up through /api/v1/me. A service token has no user, so `me` is refused:
|
|
7
|
+
* dropping the filter would silently answer "my flags" with every flag.
|
|
8
|
+
*/
|
|
9
|
+
export declare function resolveOwner(ctx: ToolContext, owner: string | undefined): Promise<string | undefined>;
|
|
10
|
+
//# sourceMappingURL=owner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"owner.d.ts","sourceRoot":"","sources":["../../src/tools/owner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,QAAQ,CAAC;AAE5B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAIhD,eAAO,MAAM,WAAW,4BAGwF,CAAC;AAEjH;;;;GAIG;AACH,wBAAsB,YAAY,CAAC,GAAG,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAY3G"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stale-flags.d.ts","sourceRoot":"","sources":["../../../src/tools/workflows/stale-flags.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAC;AAI9D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"stale-flags.d.ts","sourceRoot":"","sources":["../../../src/tools/workflows/stale-flags.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAC;AAI9D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAajD,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,WAAW,GAAG,IAAI,CAoGhF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@featureflip/mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"mcpName": "io.github.canopy-labs/featureflip",
|
|
5
5
|
"description": "MCP server for managing Featureflip feature flags from AI agents and editors",
|
|
6
6
|
"keywords": [
|
|
@@ -48,15 +48,15 @@
|
|
|
48
48
|
"smoke:docker": "node scripts/docker-smoke.mjs"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@modelcontextprotocol/server": "2.
|
|
51
|
+
"@modelcontextprotocol/server": "2.1.0",
|
|
52
52
|
"zod": "^4.6.5"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
|
-
"@modelcontextprotocol/client": "2.
|
|
55
|
+
"@modelcontextprotocol/client": "2.1.0",
|
|
56
56
|
"@typescript/typescript6": "^6.0.2",
|
|
57
57
|
"@types/node": "^26.6.2",
|
|
58
58
|
"typescript": "^7.0.2",
|
|
59
|
-
"vite": "^8.3.
|
|
59
|
+
"vite": "^8.3.1",
|
|
60
60
|
"vite-plugin-dts": "^5.1.1",
|
|
61
61
|
"vitest": "^5.0.1"
|
|
62
62
|
},
|