@featureflip/mcp 0.1.9 → 0.1.11
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 +4 -3
- package/dist/cli.mjs +1 -1
- package/dist/generated/api-types.d.ts +209 -6
- package/dist/generated/api-types.d.ts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{org-8tn1Su68.js → org-c4iWMSzU.js} +113 -63
- package/dist/tools/flags.d.ts.map +1 -1
- package/dist/tools/workflows/stale-flags.d.ts.map +1 -1
- package/package.json +7 -7
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-c4iWMSzU.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.11",
|
|
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,17 +43,17 @@ var n = {
|
|
|
43
43
|
"smoke:docker": "node scripts/docker-smoke.mjs"
|
|
44
44
|
},
|
|
45
45
|
dependencies: {
|
|
46
|
-
"@modelcontextprotocol/server": "2.1
|
|
46
|
+
"@modelcontextprotocol/server": "2.3.1",
|
|
47
47
|
zod: "^4.6.5"
|
|
48
48
|
},
|
|
49
49
|
devDependencies: {
|
|
50
|
-
"@modelcontextprotocol/client": "2.1
|
|
50
|
+
"@modelcontextprotocol/client": "2.3.1",
|
|
51
51
|
"@typescript/typescript6": "^6.0.2",
|
|
52
|
-
"@types/node": "^26.6.
|
|
52
|
+
"@types/node": "^26.6.4",
|
|
53
53
|
typescript: "^7.0.2",
|
|
54
|
-
vite: "^8.3.
|
|
55
|
-
"vite-plugin-dts": "^5.1.
|
|
56
|
-
vitest: "^5.0.
|
|
54
|
+
vite: "^8.3.3",
|
|
55
|
+
"vite-plugin-dts": "^5.1.2",
|
|
56
|
+
vitest: "^5.0.3"
|
|
57
57
|
},
|
|
58
58
|
engines: { node: ">=20.19.0" }
|
|
59
59
|
}.version, r = class extends Error {
|
|
@@ -282,6 +282,7 @@ function y(e, n) {
|
|
|
282
282
|
})).optional(),
|
|
283
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
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"),
|
|
285
|
+
isPermanent: t.boolean().optional().describe("Mark the flag permanent: a kill switch, ops toggle or entitlement gate that is meant to stay. Permanent flags are never listed as removal candidates or by find_stale_flags. Cannot be combined with expiresAtUtc"),
|
|
285
286
|
idempotency_key: t.string().optional().describe("Idempotency-Key header for safe retries")
|
|
286
287
|
}),
|
|
287
288
|
annotations: { destructiveHint: !1 }
|
|
@@ -314,7 +315,7 @@ function y(e, n) {
|
|
|
314
315
|
});
|
|
315
316
|
})), e.registerTool("set_flag_expiry", {
|
|
316
317
|
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),
|
|
318
|
+
description: "Set the date a flag is expected to be removed by, or pass expiresAtUtc: null to clear it. Expiry is advisory: evaluation never changes. Once the date passes, find_stale_flags reports the flag as expired. expiresAtUtc takes a date (2026-12-31), which means the end of that day in UTC, the same as picking it in the dashboard, or a full ISO-8601 timestamp, which is stored exactly. Refused with EXPIRY_IN_PAST for a time that has already passed (today's date is allowed), with FEATURE_NOT_ENABLED while flag expiration is not enabled for the organization, and with PERMANENT_FLAG_HAS_EXPIRY on a permanent flag (make it temporary first with set_flag_permanent). Clearing is always allowed.",
|
|
318
319
|
inputSchema: t.object({
|
|
319
320
|
project: t.string(),
|
|
320
321
|
flag: t.string().describe("Flag key or id"),
|
|
@@ -331,6 +332,25 @@ function y(e, n) {
|
|
|
331
332
|
flag: t,
|
|
332
333
|
expiresAtUtc: a
|
|
333
334
|
});
|
|
335
|
+
})), e.registerTool("set_flag_permanent", {
|
|
336
|
+
title: "Mark a flag permanent or temporary",
|
|
337
|
+
description: "Mark a flag permanent (a kill switch, ops toggle or entitlement gate that is meant to stay), or pass permanent: false to make it temporary again. Permanent flags are never listed as removal candidates or by find_stale_flags; evaluation never changes. Marking is refused with PERMANENT_FLAG_HAS_EXPIRY while the flag has an expiry date (clear it first with set_flag_expiry and expiresAtUtc: null), with FEATURE_NOT_ENABLED while permanent flags are not enabled for the organization, and on an archived flag. Making a flag temporary is always allowed.",
|
|
338
|
+
inputSchema: t.object({
|
|
339
|
+
project: t.string(),
|
|
340
|
+
flag: t.string().describe("Flag key or id"),
|
|
341
|
+
permanent: t.boolean().describe("true to mark the flag permanent, false to make it temporary")
|
|
342
|
+
}),
|
|
343
|
+
annotations: {
|
|
344
|
+
destructiveHint: !1,
|
|
345
|
+
idempotentHint: !0
|
|
346
|
+
}
|
|
347
|
+
}, async ({ project: e, flag: t, permanent: a }) => d(async () => {
|
|
348
|
+
let o = `${r(e)}/${i(t)}/permanent`;
|
|
349
|
+
return await n.api.request(a ? "PUT" : "DELETE", o), l({
|
|
350
|
+
project: e,
|
|
351
|
+
flag: t,
|
|
352
|
+
permanent: a
|
|
353
|
+
});
|
|
334
354
|
})), e.registerTool("set_flag_owner", {
|
|
335
355
|
title: "Set or clear flag owner",
|
|
336
356
|
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.",
|
|
@@ -360,7 +380,7 @@ function y(e, n) {
|
|
|
360
380
|
annotations: { destructiveHint: !0 }
|
|
361
381
|
}, async ({ project: e, flag: t }) => d(async () => (await n.api.request("DELETE", `${r(e)}/${i(t)}`), l({ deleted: t })))), e.registerTool("archive_flag", {
|
|
362
382
|
title: "Archive feature flag",
|
|
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.",
|
|
383
|
+
description: "Archive a flag (soft-hide, evaluation stops serving it). Reversible with restore_flag. Archive dependents first: refused with FLAG_HAS_DEPENDENTS while another live flag lists this one as a prerequisite, so archive those flags (blockedBy in find_stale_flags / list_removal_candidates) first. Refused with FLAG_RECENTLY_EVALUATED while live traffic is still evaluating the flag, because archiving makes every caller fall back to its own hardcoded default — normally that means the code removal has merged but not deployed yet, and the refusal clears itself once it has.",
|
|
364
384
|
inputSchema: t.object({
|
|
365
385
|
project: t.string(),
|
|
366
386
|
flag: t.string(),
|
|
@@ -677,59 +697,89 @@ function k(e, n) {
|
|
|
677
697
|
}
|
|
678
698
|
//#endregion
|
|
679
699
|
//#region src/tools/workflows/stale-flags.ts
|
|
680
|
-
var A = 50;
|
|
681
|
-
function
|
|
700
|
+
var A = t.enum(["dead", "stale"]), j = 50;
|
|
701
|
+
function M(e, n) {
|
|
682
702
|
e.registerTool("find_stale_flags", {
|
|
683
703
|
title: "Find stale flags",
|
|
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 ${
|
|
704
|
+
description: `Find flags that look ready for code cleanup: not updated in N days AND either enabled in every environment (verify rollout is complete before removing — per-rule percentage ramps are not inspected) or disabled in every environment (dead — remove flag and code path). A flag whose expiry date (set_flag_expiry) has passed is always a candidate, however recently it was edited; if it is on in some environments and off in others its reason is past-expiry, meaning the owner has to decide which way to fold it. Every result carries expiresAtUtc, expired and owner (null when unowned). Flags marked permanent (kill switches, ops toggles) are never listed. Pass owner to see one person's stale flags ("me" for your own) or "none" for the unowned ones. Every result also carries blockedBy: the live flags that still list it as a prerequisite, which have to be removed and archived before it ([] when none). blockedBy is null when the server did not classify the flag as a removal candidate, which means unknown, not unblocked. A flag that other live flags list as a prerequisite has to be removed LAST, dependents first: archiving it while a dependent still names it makes that dependent fail its prerequisite check and serve its off variation, so archive_flag refuses it with FLAG_HAS_DEPENDENTS. Checks at most ${j} candidates per call, expired flags first.`,
|
|
685
705
|
inputSchema: t.object({
|
|
686
706
|
project: t.string(),
|
|
687
707
|
days: t.number().int().min(1).optional().default(30).describe("Minimum age in days since last update"),
|
|
688
708
|
owner: g
|
|
689
709
|
}),
|
|
690
710
|
annotations: { readOnlyHint: !0 }
|
|
691
|
-
}, async ({ project: e, days: t, owner:
|
|
692
|
-
let
|
|
711
|
+
}, async ({ project: e, days: t, owner: a }) => d(async () => {
|
|
712
|
+
let o = await _(n, a), s = `/api/v1/orgs/${i(n.org)}/projects/${i(e)}/flags`, c = Date.now(), u = c - t * 24 * 60 * 60 * 1e3, d = (e) => !!e.expiresAtUtc && new Date(e.expiresAtUtc).getTime() <= c, f = [], p;
|
|
693
713
|
do {
|
|
694
|
-
let e = await n.api.request("GET",
|
|
714
|
+
let e = await n.api.request("GET", s, { query: {
|
|
695
715
|
archived: !1,
|
|
696
716
|
limit: 100,
|
|
697
|
-
cursor:
|
|
698
|
-
owner:
|
|
717
|
+
cursor: p,
|
|
718
|
+
owner: o,
|
|
719
|
+
permanent: !1
|
|
699
720
|
} });
|
|
700
|
-
|
|
701
|
-
} while (
|
|
702
|
-
|
|
703
|
-
let
|
|
704
|
-
for (let e of
|
|
705
|
-
let t = await n.api.request("GET", `${
|
|
721
|
+
f.push(...(e.items ?? []).filter((e) => !!e.key && !!e.name && !!e.updatedAt && (new Date(e.updatedAt).getTime() < u || d(e)))), p = e.next_cursor ?? void 0;
|
|
722
|
+
} while (p);
|
|
723
|
+
f.sort((e, t) => Number(d(t)) - Number(d(e)));
|
|
724
|
+
let m = f.length > j, h = f.slice(0, j), g = [];
|
|
725
|
+
for (let e of h) {
|
|
726
|
+
let t = await n.api.request("GET", `${s}/${i(e.key)}/environments`);
|
|
706
727
|
if (t.length === 0) continue;
|
|
707
|
-
let r =
|
|
708
|
-
(a ||
|
|
728
|
+
let r = d(e), a = t.every((e) => e.isEnabled), o = t.every((e) => !e.isEnabled);
|
|
729
|
+
(a || o || r) && g.push({
|
|
709
730
|
key: e.key,
|
|
710
731
|
name: e.name,
|
|
711
732
|
updatedAt: e.updatedAt,
|
|
712
733
|
expiresAtUtc: e.expiresAtUtc ?? null,
|
|
713
734
|
expired: r,
|
|
714
735
|
owner: e.owner ?? null,
|
|
715
|
-
reason: a ? "enabled-everywhere" :
|
|
716
|
-
environments: t.length
|
|
736
|
+
reason: a ? "enabled-everywhere" : o ? "disabled-everywhere" : "past-expiry",
|
|
737
|
+
environments: t.length,
|
|
738
|
+
blockedBy: null
|
|
717
739
|
});
|
|
718
740
|
}
|
|
741
|
+
let v;
|
|
742
|
+
if (g.length > 0) try {
|
|
743
|
+
let e = /* @__PURE__ */ new Map(), t;
|
|
744
|
+
do {
|
|
745
|
+
let r = await n.api.request("GET", `${s}/removal-candidates`, { query: {
|
|
746
|
+
staleness: "stale",
|
|
747
|
+
limit: 100,
|
|
748
|
+
cursor: t
|
|
749
|
+
} });
|
|
750
|
+
for (let t of r.items ?? []) t.key && t.blockedBy && e.set(t.key, t.blockedBy);
|
|
751
|
+
t = r.next_cursor ?? void 0;
|
|
752
|
+
} while (t);
|
|
753
|
+
for (let t of g) t.blockedBy = e.get(t.key) ?? null;
|
|
754
|
+
} catch (e) {
|
|
755
|
+
if (!(e instanceof r)) throw e;
|
|
756
|
+
v = `Could not read removal-candidates (${e.status} ${e.envelope.error}), so blockedBy is unknown for every flag. archive_flag still refuses any flag that a live flag depends on (FLAG_HAS_DEPENDENTS).`;
|
|
757
|
+
}
|
|
719
758
|
return l({
|
|
720
759
|
project: e,
|
|
721
760
|
olderThanDays: t,
|
|
722
|
-
...
|
|
723
|
-
checked:
|
|
724
|
-
truncated:
|
|
725
|
-
...
|
|
726
|
-
|
|
761
|
+
...o ? { owner: o } : {},
|
|
762
|
+
checked: h.length,
|
|
763
|
+
truncated: m,
|
|
764
|
+
...m ? { note: `Only the first ${j} of ${f.length} candidates were checked.` } : {},
|
|
765
|
+
...v ? { blockedByNote: v } : {},
|
|
766
|
+
stale: g
|
|
727
767
|
});
|
|
728
|
-
}))
|
|
768
|
+
})), e.registerTool("list_removal_candidates", {
|
|
769
|
+
title: "List removal candidates",
|
|
770
|
+
description: "List the flags the server is confident can be removed from code, the same list the Featureflip flag cleanup GitHub Action works from. Each item has key, reason, status (Dead or Stale), treatment (true = keep the on-branch, false = keep the off-branch) and blockedBy: the live flags that still list it as a prerequisite. Only remove a flag whose blockedBy is empty. A flag that other live flags list as a prerequisite has to be removed LAST, dependents first: archiving it while a dependent still names it makes that dependent fail its prerequisite check and serve its off variation, so archive_flag refuses it with FLAG_HAS_DEPENDENTS. staleness \"dead\" (the default) returns only dead flags, \"stale\" adds stale ones. Paginated via cursor.",
|
|
771
|
+
inputSchema: t.object({
|
|
772
|
+
project: t.string().describe("Project key"),
|
|
773
|
+
staleness: A.optional().describe("Minimum staleness tier: dead (default) or stale"),
|
|
774
|
+
limit: t.number().int().min(1).max(100).optional(),
|
|
775
|
+
cursor: t.string().optional()
|
|
776
|
+
}),
|
|
777
|
+
annotations: { readOnlyHint: !0 }
|
|
778
|
+
}, async ({ project: e, ...t }) => d(async () => l(await n.api.request("GET", `/api/v1/orgs/${i(n.org)}/projects/${i(e)}/flags/removal-candidates`, { query: t }))));
|
|
729
779
|
}
|
|
730
780
|
//#endregion
|
|
731
781
|
//#region src/tools/workflows/snippets.ts
|
|
732
|
-
var
|
|
782
|
+
var N = [
|
|
733
783
|
"js",
|
|
734
784
|
"node",
|
|
735
785
|
"browser",
|
|
@@ -741,16 +791,16 @@ var M = [
|
|
|
741
791
|
"php",
|
|
742
792
|
"ruby",
|
|
743
793
|
"swift"
|
|
744
|
-
],
|
|
794
|
+
], P = [
|
|
745
795
|
"browser",
|
|
746
796
|
"react",
|
|
747
797
|
"swift"
|
|
748
798
|
];
|
|
749
|
-
function
|
|
750
|
-
return
|
|
799
|
+
function F(e) {
|
|
800
|
+
return P.includes(e);
|
|
751
801
|
}
|
|
752
|
-
var
|
|
753
|
-
function
|
|
802
|
+
var I = "Uses a SERVER-side SDK key (Featureflip dashboard → environment → SDK keys).", L = "Uses a CLIENT-side SDK key (safe to ship to browsers/apps).";
|
|
803
|
+
function R(e, t) {
|
|
754
804
|
switch (e) {
|
|
755
805
|
case "js": return {
|
|
756
806
|
install: "npm install @featureflip/js",
|
|
@@ -765,7 +815,7 @@ const enabled = client.boolVariation('${t}', { user_id: userId }, false);
|
|
|
765
815
|
if (enabled) {
|
|
766
816
|
// new code path
|
|
767
817
|
}`,
|
|
768
|
-
notes:
|
|
818
|
+
notes: I
|
|
769
819
|
};
|
|
770
820
|
case "node": return {
|
|
771
821
|
install: "npm install @featureflip/node",
|
|
@@ -776,7 +826,7 @@ const enabled = client.boolVariation('${t}', { user_id: userId }, false);
|
|
|
776
826
|
if (enabled) {
|
|
777
827
|
// new code path
|
|
778
828
|
}`,
|
|
779
|
-
notes:
|
|
829
|
+
notes: I
|
|
780
830
|
};
|
|
781
831
|
case "browser": return {
|
|
782
832
|
install: "npm install @featureflip/browser",
|
|
@@ -788,7 +838,7 @@ const enabled = client.boolVariation('${t}', false);
|
|
|
788
838
|
if (enabled) {
|
|
789
839
|
// new code path
|
|
790
840
|
}`,
|
|
791
|
-
notes:
|
|
841
|
+
notes: L
|
|
792
842
|
};
|
|
793
843
|
case "react": return {
|
|
794
844
|
install: "npm install @featureflip/react @featureflip/browser",
|
|
@@ -801,7 +851,7 @@ function MyComponent() {
|
|
|
801
851
|
const enabled = useFeatureFlag('${t}', false);
|
|
802
852
|
return enabled ? <NewExperience /> : <CurrentExperience />;
|
|
803
853
|
}`,
|
|
804
|
-
notes:
|
|
854
|
+
notes: L
|
|
805
855
|
};
|
|
806
856
|
case "python": return {
|
|
807
857
|
install: "pip install featureflip",
|
|
@@ -812,7 +862,7 @@ enabled = client.variation("${t}", {"user_id": user_id}, default=False)
|
|
|
812
862
|
if enabled:
|
|
813
863
|
# new code path
|
|
814
864
|
...`,
|
|
815
|
-
notes:
|
|
865
|
+
notes: I
|
|
816
866
|
};
|
|
817
867
|
case "csharp": return {
|
|
818
868
|
install: "dotnet add package Featureflip.Client",
|
|
@@ -825,7 +875,7 @@ if (enabled)
|
|
|
825
875
|
{
|
|
826
876
|
// new code path
|
|
827
877
|
}`,
|
|
828
|
-
notes:
|
|
878
|
+
notes: I
|
|
829
879
|
};
|
|
830
880
|
case "java": return {
|
|
831
881
|
install: "implementation 'io.featureflip:featureflip-java:2.0.0'",
|
|
@@ -839,7 +889,7 @@ boolean enabled = client.boolVariation("${t}",
|
|
|
839
889
|
if (enabled) {
|
|
840
890
|
// new code path
|
|
841
891
|
}`,
|
|
842
|
-
notes:
|
|
892
|
+
notes: I
|
|
843
893
|
};
|
|
844
894
|
case "go": return {
|
|
845
895
|
install: "go get github.com/canopy-labs/featureflip-go/v2",
|
|
@@ -855,7 +905,7 @@ ctx := featureflip.EvaluationContext{UserID: userID}
|
|
|
855
905
|
if client.BoolVariation("${t}", ctx, false) {
|
|
856
906
|
// new code path
|
|
857
907
|
}`,
|
|
858
|
-
notes:
|
|
908
|
+
notes: I
|
|
859
909
|
};
|
|
860
910
|
case "php": return {
|
|
861
911
|
install: "composer require featureflip/featureflip-php",
|
|
@@ -870,7 +920,7 @@ $enabled = $client->boolVariation('${t}', ['user_id' => $userId], false);
|
|
|
870
920
|
if ($enabled) {
|
|
871
921
|
// new code path
|
|
872
922
|
}`,
|
|
873
|
-
notes:
|
|
923
|
+
notes: I + " Requires PSR-16 cache and PSR-18 HTTP implementations."
|
|
874
924
|
};
|
|
875
925
|
case "ruby": return {
|
|
876
926
|
install: "gem \"featureflip\"",
|
|
@@ -881,7 +931,7 @@ enabled = client.bool_variation("${t}", { "user_id" => user_id }, false)
|
|
|
881
931
|
if enabled
|
|
882
932
|
# new code path
|
|
883
933
|
end`,
|
|
884
|
-
notes:
|
|
934
|
+
notes: I
|
|
885
935
|
};
|
|
886
936
|
case "swift": return {
|
|
887
937
|
install: ".package(url: \"https://github.com/canopy-labs/featureflip-swift\", from: \"2.0.0\")",
|
|
@@ -894,13 +944,13 @@ let enabled = client.boolVariation("${t}", default: false)
|
|
|
894
944
|
if enabled {
|
|
895
945
|
// new code path
|
|
896
946
|
}`,
|
|
897
|
-
notes:
|
|
947
|
+
notes: L
|
|
898
948
|
};
|
|
899
949
|
}
|
|
900
950
|
}
|
|
901
951
|
//#endregion
|
|
902
952
|
//#region src/tools/workflows/wrap-feature.ts
|
|
903
|
-
function
|
|
953
|
+
function z(e, n) {
|
|
904
954
|
e.registerTool("wrap_feature", {
|
|
905
955
|
title: "Wrap a feature in a new flag",
|
|
906
956
|
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.",
|
|
@@ -910,7 +960,7 @@ function R(e, n) {
|
|
|
910
960
|
name: t.string().optional().describe("Display name; defaults to the key"),
|
|
911
961
|
description: t.string().optional().describe("What this flag guards"),
|
|
912
962
|
tags: t.array(t.string()).optional(),
|
|
913
|
-
language: t.enum(
|
|
963
|
+
language: t.enum(N).describe("SDK language of the codebase being edited"),
|
|
914
964
|
idempotency_key: t.string().optional()
|
|
915
965
|
}),
|
|
916
966
|
annotations: { destructiveHint: !1 }
|
|
@@ -922,10 +972,10 @@ function R(e, n) {
|
|
|
922
972
|
type: "Boolean",
|
|
923
973
|
description: a,
|
|
924
974
|
tags: o,
|
|
925
|
-
...
|
|
975
|
+
...F(s) ? { clientSideVisible: !0 } : {}
|
|
926
976
|
},
|
|
927
977
|
idempotencyKey: c
|
|
928
|
-
}), d =
|
|
978
|
+
}), d = R(s, t);
|
|
929
979
|
return l({
|
|
930
980
|
flag: u,
|
|
931
981
|
integration: {
|
|
@@ -944,17 +994,17 @@ function R(e, n) {
|
|
|
944
994
|
}
|
|
945
995
|
//#endregion
|
|
946
996
|
//#region src/server.ts
|
|
947
|
-
function
|
|
997
|
+
function B(t) {
|
|
948
998
|
let r = new e({
|
|
949
999
|
name: "featureflip",
|
|
950
1000
|
version: n
|
|
951
1001
|
});
|
|
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),
|
|
1002
|
+
return p(r, t), m(r, t), h(r, t), y(r, t), x(r, t), w(r, t), T(r, t), k(r, t), M(r, t), z(r, t), r;
|
|
953
1003
|
}
|
|
954
1004
|
//#endregion
|
|
955
1005
|
//#region src/config.ts
|
|
956
|
-
var
|
|
957
|
-
function
|
|
1006
|
+
var V = "FEATUREFLIP_TOKEN is required. Create one in the Featureflip dashboard — Settings → API Tokens (personal token, ffp_...) or Organization Settings → Service Tokens (ffs_...) — and set it in your MCP client config.";
|
|
1007
|
+
function H(e = process.env) {
|
|
958
1008
|
let t = e.FEATUREFLIP_TOKEN?.trim();
|
|
959
1009
|
return t ? {
|
|
960
1010
|
token: t,
|
|
@@ -962,14 +1012,14 @@ function V(e = process.env) {
|
|
|
962
1012
|
org: e.FEATUREFLIP_ORG?.trim() || void 0
|
|
963
1013
|
} : null;
|
|
964
1014
|
}
|
|
965
|
-
function
|
|
966
|
-
let t =
|
|
967
|
-
if (!t) throw Error(
|
|
1015
|
+
function U(e = process.env) {
|
|
1016
|
+
let t = H(e);
|
|
1017
|
+
if (!t) throw Error(V);
|
|
968
1018
|
return t;
|
|
969
1019
|
}
|
|
970
1020
|
//#endregion
|
|
971
1021
|
//#region src/org.ts
|
|
972
|
-
async function
|
|
1022
|
+
async function W(e, t) {
|
|
973
1023
|
if (t) return t;
|
|
974
1024
|
let n = (await e.request("GET", "/api/v1/orgs", { query: { limit: 100 } })).items ?? [];
|
|
975
1025
|
if (n.length === 1) {
|
|
@@ -980,4 +1030,4 @@ async function U(e, t) {
|
|
|
980
1030
|
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(", "));
|
|
981
1031
|
}
|
|
982
1032
|
//#endregion
|
|
983
|
-
export {
|
|
1033
|
+
export { B as a, n as c, H as i, V as n, r as o, U as r, c as s, W as t };
|
|
@@ -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;AAKhD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,WAAW,GAAG,IAAI,
|
|
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,CAqS3E"}
|
|
@@ -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;AAsBjD,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,WAAW,GAAG,IAAI,CAsKhF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@featureflip/mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
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,17 +48,17 @@
|
|
|
48
48
|
"smoke:docker": "node scripts/docker-smoke.mjs"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@modelcontextprotocol/server": "2.1
|
|
51
|
+
"@modelcontextprotocol/server": "2.3.1",
|
|
52
52
|
"zod": "^4.6.5"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
|
-
"@modelcontextprotocol/client": "2.1
|
|
55
|
+
"@modelcontextprotocol/client": "2.3.1",
|
|
56
56
|
"@typescript/typescript6": "^6.0.2",
|
|
57
|
-
"@types/node": "^26.6.
|
|
57
|
+
"@types/node": "^26.6.4",
|
|
58
58
|
"typescript": "^7.0.2",
|
|
59
|
-
"vite": "^8.3.
|
|
60
|
-
"vite-plugin-dts": "^5.1.
|
|
61
|
-
"vitest": "^5.0.
|
|
59
|
+
"vite": "^8.3.3",
|
|
60
|
+
"vite-plugin-dts": "^5.1.2",
|
|
61
|
+
"vitest": "^5.0.3"
|
|
62
62
|
},
|
|
63
63
|
"engines": {
|
|
64
64
|
"node": ">=20.19.0"
|