@featureflip/mcp 0.1.0
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/LICENSE +190 -0
- package/README.md +56 -0
- package/bin/featureflip-mcp.mjs +2 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.mjs +20 -0
- package/dist/client.d.ts +34 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/generated/api-types.d.ts +4952 -0
- package/dist/generated/api-types.d.ts.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +2 -0
- package/dist/org-yZkTaCl5.js +719 -0
- package/dist/org.d.ts +3 -0
- package/dist/org.d.ts.map +1 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/tools/context.d.ts +6 -0
- package/dist/tools/context.d.ts.map +1 -0
- package/dist/tools/environments.d.ts +4 -0
- package/dist/tools/environments.d.ts.map +1 -0
- package/dist/tools/flag-environments.d.ts +4 -0
- package/dist/tools/flag-environments.d.ts.map +1 -0
- package/dist/tools/flags.d.ts +4 -0
- package/dist/tools/flags.d.ts.map +1 -0
- package/dist/tools/projects.d.ts +4 -0
- package/dist/tools/projects.d.ts.map +1 -0
- package/dist/tools/segments.d.ts +4 -0
- package/dist/tools/segments.d.ts.map +1 -0
- package/dist/tools/targeting.d.ts +4 -0
- package/dist/tools/targeting.d.ts.map +1 -0
- package/dist/tools/variations.d.ts +4 -0
- package/dist/tools/variations.d.ts.map +1 -0
- package/dist/tools/workflows/snippets.d.ts +9 -0
- package/dist/tools/workflows/snippets.d.ts.map +1 -0
- package/dist/tools/workflows/stale-flags.d.ts +4 -0
- package/dist/tools/workflows/stale-flags.d.ts.map +1 -0
- package/dist/tools/workflows/wrap-feature.d.ts +4 -0
- package/dist/tools/workflows/wrap-feature.d.ts.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/package.json +44 -0
|
@@ -0,0 +1,719 @@
|
|
|
1
|
+
import { McpServer as e } from "@modelcontextprotocol/server";
|
|
2
|
+
import * as t from "zod/v4";
|
|
3
|
+
//#endregion
|
|
4
|
+
//#region src/version.ts
|
|
5
|
+
var n = {
|
|
6
|
+
name: "@featureflip/mcp",
|
|
7
|
+
version: "0.1.0",
|
|
8
|
+
description: "MCP server for managing Featureflip feature flags from AI agents and editors",
|
|
9
|
+
license: "Apache-2.0",
|
|
10
|
+
repository: {
|
|
11
|
+
type: "git",
|
|
12
|
+
url: "https://github.com/canopy-labs/featureflip-mcp.git"
|
|
13
|
+
},
|
|
14
|
+
homepage: "https://featureflip.io/docs/integrations/mcp/",
|
|
15
|
+
type: "module",
|
|
16
|
+
bin: { "featureflip-mcp": "bin/featureflip-mcp.mjs" },
|
|
17
|
+
exports: { ".": {
|
|
18
|
+
types: "./dist/index.d.ts",
|
|
19
|
+
import: "./dist/index.mjs"
|
|
20
|
+
} },
|
|
21
|
+
types: "./dist/index.d.ts",
|
|
22
|
+
files: ["dist", "bin"],
|
|
23
|
+
publishConfig: { access: "public" },
|
|
24
|
+
scripts: {
|
|
25
|
+
build: "vite build",
|
|
26
|
+
typecheck: "tsc --noEmit -p tsconfig.json",
|
|
27
|
+
"generate:api": "npx --yes openapi-typescript@7.13.0 ../../apps/management-api/public-v1-openapi.json -o src/generated/api-types.ts",
|
|
28
|
+
test: "vitest run",
|
|
29
|
+
"test:watch": "vitest"
|
|
30
|
+
},
|
|
31
|
+
dependencies: {
|
|
32
|
+
"@modelcontextprotocol/server": "2.0.0-beta.3",
|
|
33
|
+
zod: "^4.0.0"
|
|
34
|
+
},
|
|
35
|
+
devDependencies: {
|
|
36
|
+
"@modelcontextprotocol/client": "2.0.0-beta.3",
|
|
37
|
+
"@types/node": "^26.0.0",
|
|
38
|
+
typescript: "^7.0.2",
|
|
39
|
+
vite: "^8.0.16",
|
|
40
|
+
"vite-plugin-dts": "^5.0.0",
|
|
41
|
+
vitest: "^4.1.2"
|
|
42
|
+
},
|
|
43
|
+
engines: { node: ">=20.19.0" }
|
|
44
|
+
}.version, r = class extends Error {
|
|
45
|
+
status;
|
|
46
|
+
envelope;
|
|
47
|
+
constructor(e, t) {
|
|
48
|
+
super(t.message), this.status = e, this.envelope = t, this.name = "ApiError";
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
function i(e) {
|
|
52
|
+
return encodeURIComponent(e);
|
|
53
|
+
}
|
|
54
|
+
var a = class {
|
|
55
|
+
opts;
|
|
56
|
+
fetchImpl;
|
|
57
|
+
constructor(e) {
|
|
58
|
+
this.opts = e, this.fetchImpl = e.fetchImpl ?? fetch;
|
|
59
|
+
}
|
|
60
|
+
async request(e, t, n = {}) {
|
|
61
|
+
let i = new URL(this.opts.baseUrl + t);
|
|
62
|
+
for (let [e, t] of Object.entries(n.query ?? {})) t !== void 0 && i.searchParams.set(e, String(t));
|
|
63
|
+
let a = {
|
|
64
|
+
Authorization: `Bearer ${this.opts.token}`,
|
|
65
|
+
Accept: "application/json"
|
|
66
|
+
}, o;
|
|
67
|
+
n.body !== void 0 && (a["Content-Type"] = "application/json", o = JSON.stringify(n.body)), n.idempotencyKey && (a["Idempotency-Key"] = n.idempotencyKey);
|
|
68
|
+
let s = await this.fetchImpl(i.toString(), {
|
|
69
|
+
method: e,
|
|
70
|
+
headers: a,
|
|
71
|
+
body: o
|
|
72
|
+
});
|
|
73
|
+
if (s.status !== 204) {
|
|
74
|
+
if (!s.ok) {
|
|
75
|
+
let e;
|
|
76
|
+
try {
|
|
77
|
+
if (e = await s.json(), typeof e?.error != "string") throw Error("not an envelope");
|
|
78
|
+
} catch {
|
|
79
|
+
e = {
|
|
80
|
+
error: `http_${s.status}`,
|
|
81
|
+
message: `HTTP ${s.status} from ${i.pathname}`
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
throw new r(s.status, e);
|
|
85
|
+
}
|
|
86
|
+
return await s.json();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/errors.ts
|
|
92
|
+
function o(e) {
|
|
93
|
+
return { content: [{
|
|
94
|
+
type: "text",
|
|
95
|
+
text: JSON.stringify(e, null, 2)
|
|
96
|
+
}] };
|
|
97
|
+
}
|
|
98
|
+
function s(e) {
|
|
99
|
+
if (e instanceof r) {
|
|
100
|
+
let t = e.envelope, n = [`Featureflip API error ${e.status} (${t.error}): ${t.message}`];
|
|
101
|
+
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}`), {
|
|
102
|
+
content: [{
|
|
103
|
+
type: "text",
|
|
104
|
+
text: n.join("\n")
|
|
105
|
+
}],
|
|
106
|
+
isError: !0
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
content: [{
|
|
111
|
+
type: "text",
|
|
112
|
+
text: `Error: ${e instanceof Error ? e.message : String(e)}`
|
|
113
|
+
}],
|
|
114
|
+
isError: !0
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
async function c(e) {
|
|
118
|
+
try {
|
|
119
|
+
return await e();
|
|
120
|
+
} catch (e) {
|
|
121
|
+
return s(e);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
//#endregion
|
|
125
|
+
//#region src/tools/projects.ts
|
|
126
|
+
var l = {
|
|
127
|
+
limit: t.number().int().min(1).max(100).optional().describe("Page size (default 20)"),
|
|
128
|
+
cursor: t.string().optional().describe("next_cursor from a previous response")
|
|
129
|
+
};
|
|
130
|
+
function u(e, n) {
|
|
131
|
+
e.registerTool("list_projects", {
|
|
132
|
+
title: "List projects",
|
|
133
|
+
description: "List projects in the Featureflip organization. Paginated: pass cursor from next_cursor to continue.",
|
|
134
|
+
inputSchema: t.object({ ...l }),
|
|
135
|
+
annotations: { readOnlyHint: !0 }
|
|
136
|
+
}, async ({ limit: e, cursor: t }) => c(async () => o(await n.api.request("GET", `/api/v1/orgs/${i(n.org)}/projects`, { query: {
|
|
137
|
+
limit: e,
|
|
138
|
+
cursor: t
|
|
139
|
+
} }))));
|
|
140
|
+
}
|
|
141
|
+
//#endregion
|
|
142
|
+
//#region src/tools/environments.ts
|
|
143
|
+
function d(e, n) {
|
|
144
|
+
e.registerTool("list_environments", {
|
|
145
|
+
title: "List environments",
|
|
146
|
+
description: "List the environments of a project (e.g. development, staging, production).",
|
|
147
|
+
inputSchema: t.object({
|
|
148
|
+
project: t.string().describe("Project key"),
|
|
149
|
+
limit: t.number().int().min(1).max(100).optional(),
|
|
150
|
+
cursor: t.string().optional()
|
|
151
|
+
}),
|
|
152
|
+
annotations: { readOnlyHint: !0 }
|
|
153
|
+
}, async ({ project: e, limit: t, cursor: r }) => c(async () => o(await n.api.request("GET", `/api/v1/orgs/${i(n.org)}/projects/${i(e)}/environments`, { query: {
|
|
154
|
+
limit: t,
|
|
155
|
+
cursor: r
|
|
156
|
+
} }))));
|
|
157
|
+
}
|
|
158
|
+
//#endregion
|
|
159
|
+
//#region src/tools/segments.ts
|
|
160
|
+
function f(e, n) {
|
|
161
|
+
e.registerTool("list_segments", {
|
|
162
|
+
title: "List user segments",
|
|
163
|
+
description: "List reusable user segments in a project. Segments are referenced from targeting rules via userSegmentId.",
|
|
164
|
+
inputSchema: t.object({
|
|
165
|
+
project: t.string().describe("Project key"),
|
|
166
|
+
limit: t.number().int().min(1).max(100).optional(),
|
|
167
|
+
cursor: t.string().optional()
|
|
168
|
+
}),
|
|
169
|
+
annotations: { readOnlyHint: !0 }
|
|
170
|
+
}, async ({ project: e, limit: t, cursor: r }) => c(async () => o(await n.api.request("GET", `/api/v1/orgs/${i(n.org)}/projects/${i(e)}/segments`, { query: {
|
|
171
|
+
limit: t,
|
|
172
|
+
cursor: r
|
|
173
|
+
} })))), e.registerTool("get_segment", {
|
|
174
|
+
title: "Get user segment",
|
|
175
|
+
description: "Get one user segment (its conditions, id, and metadata) by key or id.",
|
|
176
|
+
inputSchema: t.object({
|
|
177
|
+
project: t.string().describe("Project key"),
|
|
178
|
+
segment: t.string().describe("Segment key or id")
|
|
179
|
+
}),
|
|
180
|
+
annotations: { readOnlyHint: !0 }
|
|
181
|
+
}, async ({ project: e, segment: t }) => c(async () => o(await n.api.request("GET", `/api/v1/orgs/${i(n.org)}/projects/${i(e)}/segments/${i(t)}`))));
|
|
182
|
+
}
|
|
183
|
+
//#endregion
|
|
184
|
+
//#region src/tools/flags.ts
|
|
185
|
+
var p = t.enum([
|
|
186
|
+
"Boolean",
|
|
187
|
+
"String",
|
|
188
|
+
"Number",
|
|
189
|
+
"Json"
|
|
190
|
+
]);
|
|
191
|
+
function m(e, n) {
|
|
192
|
+
let r = (e) => `/api/v1/orgs/${i(n.org)}/projects/${i(e)}/flags`;
|
|
193
|
+
e.registerTool("list_flags", {
|
|
194
|
+
title: "List feature flags",
|
|
195
|
+
description: "List feature flags in a project. Filter with search (key/name substring), tag, type, archived. Paginated via cursor.",
|
|
196
|
+
inputSchema: t.object({
|
|
197
|
+
project: t.string().describe("Project key"),
|
|
198
|
+
search: t.string().optional(),
|
|
199
|
+
tag: t.string().optional(),
|
|
200
|
+
type: p.optional(),
|
|
201
|
+
archived: t.boolean().optional().describe("true = only archived, false = only active"),
|
|
202
|
+
limit: t.number().int().min(1).max(100).optional(),
|
|
203
|
+
cursor: t.string().optional()
|
|
204
|
+
}),
|
|
205
|
+
annotations: { readOnlyHint: !0 }
|
|
206
|
+
}, async ({ project: e, ...t }) => c(async () => o(await n.api.request("GET", r(e), { query: t })))), e.registerTool("get_flag", {
|
|
207
|
+
title: "Get feature flag",
|
|
208
|
+
description: "Get one feature flag with its variations and metadata. Address by flag key or id.",
|
|
209
|
+
inputSchema: t.object({
|
|
210
|
+
project: t.string().describe("Project key"),
|
|
211
|
+
flag: t.string().describe("Flag key or id")
|
|
212
|
+
}),
|
|
213
|
+
annotations: { readOnlyHint: !0 }
|
|
214
|
+
}, async ({ project: e, flag: t }) => c(async () => o(await n.api.request("GET", `${r(e)}/${i(t)}`)))), e.registerTool("create_flag", {
|
|
215
|
+
title: "Create feature flag",
|
|
216
|
+
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).",
|
|
217
|
+
inputSchema: t.object({
|
|
218
|
+
project: t.string().describe("Project key"),
|
|
219
|
+
key: t.string().describe("Unique flag key, e.g. checkout-v2"),
|
|
220
|
+
name: t.string(),
|
|
221
|
+
type: p,
|
|
222
|
+
description: t.string().optional(),
|
|
223
|
+
tags: t.array(t.string()).optional(),
|
|
224
|
+
clientSideVisible: t.boolean().optional().describe("Expose to client-side SDKs (browser/mobile)"),
|
|
225
|
+
initialVariations: t.array(t.object({
|
|
226
|
+
key: t.string(),
|
|
227
|
+
name: t.string().optional(),
|
|
228
|
+
value: t.string().describe("Variation value serialized as a string"),
|
|
229
|
+
description: t.string().optional()
|
|
230
|
+
})).optional(),
|
|
231
|
+
idempotency_key: t.string().optional().describe("Idempotency-Key header for safe retries")
|
|
232
|
+
}),
|
|
233
|
+
annotations: {}
|
|
234
|
+
}, async ({ project: e, idempotency_key: t, ...i }) => c(async () => o(await n.api.request("POST", r(e), {
|
|
235
|
+
body: i,
|
|
236
|
+
idempotencyKey: t
|
|
237
|
+
})))), e.registerTool("update_flag", {
|
|
238
|
+
title: "Update feature flag metadata",
|
|
239
|
+
description: "Update flag name/description/tags/clientSideVisible. Key and type are immutable. Use toggle_flag / update_targeting for behavior changes.",
|
|
240
|
+
inputSchema: t.object({
|
|
241
|
+
project: t.string(),
|
|
242
|
+
flag: t.string().describe("Flag key or id"),
|
|
243
|
+
name: t.string().optional(),
|
|
244
|
+
description: t.string().optional(),
|
|
245
|
+
tags: t.array(t.string()).optional(),
|
|
246
|
+
clientSideVisible: t.boolean().optional()
|
|
247
|
+
}),
|
|
248
|
+
annotations: {}
|
|
249
|
+
}, async ({ project: e, flag: t, ...a }) => c(async () => (await n.api.request("PUT", `${r(e)}/${i(t)}`, { body: a }), o({
|
|
250
|
+
project: e,
|
|
251
|
+
flag: t,
|
|
252
|
+
updated: !0
|
|
253
|
+
})))), e.registerTool("delete_flag", {
|
|
254
|
+
title: "Delete feature flag",
|
|
255
|
+
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.",
|
|
256
|
+
inputSchema: t.object({
|
|
257
|
+
project: t.string(),
|
|
258
|
+
flag: t.string().describe("Flag key or id")
|
|
259
|
+
}),
|
|
260
|
+
annotations: { destructiveHint: !0 }
|
|
261
|
+
}, async ({ project: e, flag: t }) => c(async () => (await n.api.request("DELETE", `${r(e)}/${i(t)}`), o({ deleted: t })))), e.registerTool("archive_flag", {
|
|
262
|
+
title: "Archive feature flag",
|
|
263
|
+
description: "Archive a flag (soft-hide, evaluation stops serving it). Reversible with restore_flag.",
|
|
264
|
+
inputSchema: t.object({
|
|
265
|
+
project: t.string(),
|
|
266
|
+
flag: t.string()
|
|
267
|
+
}),
|
|
268
|
+
annotations: { destructiveHint: !0 }
|
|
269
|
+
}, async ({ project: e, flag: t }) => c(async () => (await n.api.request("POST", `${r(e)}/${i(t)}/archive`), o({
|
|
270
|
+
flag: t,
|
|
271
|
+
archived: !0
|
|
272
|
+
})))), e.registerTool("restore_flag", {
|
|
273
|
+
title: "Restore archived flag",
|
|
274
|
+
description: "Restore a previously archived flag.",
|
|
275
|
+
inputSchema: t.object({
|
|
276
|
+
project: t.string(),
|
|
277
|
+
flag: t.string()
|
|
278
|
+
}),
|
|
279
|
+
annotations: {}
|
|
280
|
+
}, async ({ project: e, flag: t }) => c(async () => (await n.api.request("POST", `${r(e)}/${i(t)}/restore`), o({
|
|
281
|
+
flag: t,
|
|
282
|
+
archived: !1
|
|
283
|
+
}))));
|
|
284
|
+
}
|
|
285
|
+
//#endregion
|
|
286
|
+
//#region src/tools/flag-environments.ts
|
|
287
|
+
var h = t.enum([
|
|
288
|
+
"SingleVariation",
|
|
289
|
+
"PercentageRollout",
|
|
290
|
+
"TargetedRollout"
|
|
291
|
+
]);
|
|
292
|
+
function g(e, n) {
|
|
293
|
+
let r = (e, t) => `/api/v1/orgs/${i(n.org)}/projects/${i(e)}/flags/${i(t)}`;
|
|
294
|
+
e.registerTool("toggle_flag", {
|
|
295
|
+
title: "Toggle flag in environment",
|
|
296
|
+
description: "Enable or disable a flag in ONE environment. Affects live evaluation immediately — double-check the environment.",
|
|
297
|
+
inputSchema: t.object({
|
|
298
|
+
project: t.string(),
|
|
299
|
+
flag: t.string().describe("Flag key or id"),
|
|
300
|
+
environment: t.string().describe("Environment key, e.g. production"),
|
|
301
|
+
enabled: t.boolean()
|
|
302
|
+
}),
|
|
303
|
+
annotations: { destructiveHint: !0 }
|
|
304
|
+
}, async ({ project: e, flag: t, environment: a, enabled: s }) => c(async () => (await n.api.request("POST", `${r(e, t)}/environments/${i(a)}/toggle`, { body: { enabled: s } }), o({
|
|
305
|
+
project: e,
|
|
306
|
+
flag: t,
|
|
307
|
+
environment: a,
|
|
308
|
+
enabled: s
|
|
309
|
+
})))), e.registerTool("update_flag_environment_config", {
|
|
310
|
+
title: "Update flag environment config",
|
|
311
|
+
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.",
|
|
312
|
+
inputSchema: t.object({
|
|
313
|
+
project: t.string(),
|
|
314
|
+
flag: t.string(),
|
|
315
|
+
environment: t.string(),
|
|
316
|
+
defaultVariationId: t.string().describe("Variation id to serve on fallthrough — required by the API"),
|
|
317
|
+
strategy: h.optional(),
|
|
318
|
+
prerequisites: t.array(t.object({
|
|
319
|
+
prerequisiteFlagKey: t.string(),
|
|
320
|
+
expectedVariationKey: t.string()
|
|
321
|
+
})).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.")
|
|
322
|
+
}),
|
|
323
|
+
annotations: { destructiveHint: !0 }
|
|
324
|
+
}, async ({ project: e, flag: t, environment: a, ...s }) => c(async () => (await n.api.request("PUT", `${r(e, t)}/environments/${i(a)}`, { body: s }), o({
|
|
325
|
+
project: e,
|
|
326
|
+
flag: t,
|
|
327
|
+
environment: a,
|
|
328
|
+
...s
|
|
329
|
+
})))), e.registerTool("flag_status", {
|
|
330
|
+
title: "Flag status across environments",
|
|
331
|
+
description: "Compact cross-environment view of one flag: enabled state, strategy, default variation, and prerequisites per environment.",
|
|
332
|
+
inputSchema: t.object({
|
|
333
|
+
project: t.string(),
|
|
334
|
+
flag: t.string()
|
|
335
|
+
}),
|
|
336
|
+
annotations: { readOnlyHint: !0 }
|
|
337
|
+
}, async ({ project: e, flag: t }) => c(async () => {
|
|
338
|
+
let [i, a] = await Promise.all([n.api.request("GET", r(e, t)), n.api.request("GET", `${r(e, t)}/environments`)]), s = (e) => (i.variations ?? []).find((t) => t.id === e)?.key ?? e ?? null;
|
|
339
|
+
return o({
|
|
340
|
+
key: i.key ?? null,
|
|
341
|
+
name: i.name ?? null,
|
|
342
|
+
type: i.type ?? null,
|
|
343
|
+
archived: i.isArchived ?? !1,
|
|
344
|
+
updatedAt: i.updatedAt ?? null,
|
|
345
|
+
environments: a.map((e) => ({
|
|
346
|
+
environment: e.environmentKey ?? null,
|
|
347
|
+
enabled: e.isEnabled ?? !1,
|
|
348
|
+
strategy: e.strategy ?? null,
|
|
349
|
+
defaultVariation: s(e.defaultVariationId),
|
|
350
|
+
prerequisites: e.prerequisites ?? []
|
|
351
|
+
}))
|
|
352
|
+
});
|
|
353
|
+
}));
|
|
354
|
+
}
|
|
355
|
+
//#endregion
|
|
356
|
+
//#region src/tools/targeting.ts
|
|
357
|
+
var _ = t.object({
|
|
358
|
+
attribute: t.string().describe("Context attribute name, e.g. plan, country, user_id"),
|
|
359
|
+
operator: t.string().describe("OperatorType member name, case-sensitive — e.g. Equals, NotEquals, Contains, In, GreaterThan"),
|
|
360
|
+
values: t.array(t.string()),
|
|
361
|
+
negate: t.boolean().optional().default(!1)
|
|
362
|
+
}), v = t.object({
|
|
363
|
+
description: t.string().optional(),
|
|
364
|
+
variationId: t.string().optional().describe("Variation served when the rule matches"),
|
|
365
|
+
rolloutPercentage: t.number().int().min(0).max(100).optional().describe("Percentage rollout for this rule"),
|
|
366
|
+
userSegmentId: t.string().optional().describe("Match users in this segment (see list_segments)"),
|
|
367
|
+
conditionGroups: t.array(t.object({
|
|
368
|
+
operator: t.enum(["And", "Or"]).describe("How conditions inside this group combine"),
|
|
369
|
+
conditions: t.array(_)
|
|
370
|
+
})).optional().describe("Groups are ANDed together; conditions inside a group use the group operator")
|
|
371
|
+
});
|
|
372
|
+
function y(e, n) {
|
|
373
|
+
let r = (e, t, r) => `/api/v1/orgs/${i(n.org)}/projects/${i(e)}/flags/${i(t)}/environments/${i(r)}/targeting`;
|
|
374
|
+
e.registerTool("get_targeting", {
|
|
375
|
+
title: "Get targeting rules",
|
|
376
|
+
description: "Get a flag's current targeting configuration (enabled flag + ordered rules) in one environment.",
|
|
377
|
+
inputSchema: t.object({
|
|
378
|
+
project: t.string(),
|
|
379
|
+
flag: t.string(),
|
|
380
|
+
environment: t.string()
|
|
381
|
+
}),
|
|
382
|
+
annotations: { readOnlyHint: !0 }
|
|
383
|
+
}, async ({ project: e, flag: t, environment: i }) => c(async () => o(await n.api.request("GET", r(e, t, i))))), e.registerTool("update_targeting", {
|
|
384
|
+
title: "Replace targeting rules",
|
|
385
|
+
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.",
|
|
386
|
+
inputSchema: t.object({
|
|
387
|
+
project: t.string(),
|
|
388
|
+
flag: t.string(),
|
|
389
|
+
environment: t.string(),
|
|
390
|
+
rules: t.array(v)
|
|
391
|
+
}),
|
|
392
|
+
annotations: { destructiveHint: !0 }
|
|
393
|
+
}, async ({ project: e, flag: t, environment: i, rules: a }) => c(async () => (await n.api.request("PUT", r(e, t, i), { body: { rules: a } }), o({
|
|
394
|
+
project: e,
|
|
395
|
+
flag: t,
|
|
396
|
+
environment: i,
|
|
397
|
+
rulesReplaced: a.length
|
|
398
|
+
}))));
|
|
399
|
+
}
|
|
400
|
+
//#endregion
|
|
401
|
+
//#region src/tools/variations.ts
|
|
402
|
+
function b(e, n) {
|
|
403
|
+
e.registerTool("manage_variation", {
|
|
404
|
+
title: "Add, update, or remove a variation",
|
|
405
|
+
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.",
|
|
406
|
+
inputSchema: t.object({
|
|
407
|
+
project: t.string(),
|
|
408
|
+
flag: t.string(),
|
|
409
|
+
action: t.enum([
|
|
410
|
+
"add",
|
|
411
|
+
"update",
|
|
412
|
+
"remove"
|
|
413
|
+
]),
|
|
414
|
+
variationId: t.string().optional().describe("Required for update/remove"),
|
|
415
|
+
key: t.string().optional().describe("Required for add; immutable afterwards"),
|
|
416
|
+
name: t.string().optional(),
|
|
417
|
+
value: t.string().optional().describe("Variation value serialized as a string; required for add"),
|
|
418
|
+
description: t.string().optional()
|
|
419
|
+
}),
|
|
420
|
+
annotations: { destructiveHint: !0 }
|
|
421
|
+
}, async ({ project: e, flag: t, action: r, variationId: a, key: l, name: u, value: d, description: f }) => c(async () => {
|
|
422
|
+
let c = `/api/v1/orgs/${i(n.org)}/projects/${i(e)}/flags/${i(t)}/variations`;
|
|
423
|
+
return r === "add" ? !l || d === void 0 ? s(/* @__PURE__ */ Error("action=add requires key and value")) : o(await n.api.request("POST", c, { body: {
|
|
424
|
+
key: l,
|
|
425
|
+
name: u,
|
|
426
|
+
value: d,
|
|
427
|
+
description: f
|
|
428
|
+
} })) : a ? r === "update" ? (await n.api.request("PUT", `${c}/${i(a)}`, { body: {
|
|
429
|
+
name: u,
|
|
430
|
+
value: d,
|
|
431
|
+
description: f
|
|
432
|
+
} }), o({ updated: a })) : (await n.api.request("DELETE", `${c}/${i(a)}`), o({ removed: a })) : s(/* @__PURE__ */ Error(`action=${r} requires variationId (see get_flag for ids)`));
|
|
433
|
+
}));
|
|
434
|
+
}
|
|
435
|
+
//#endregion
|
|
436
|
+
//#region src/tools/workflows/stale-flags.ts
|
|
437
|
+
var x = 50;
|
|
438
|
+
function S(e, n) {
|
|
439
|
+
e.registerTool("find_stale_flags", {
|
|
440
|
+
title: "Find stale flags",
|
|
441
|
+
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 ${x} candidates per call.`,
|
|
442
|
+
inputSchema: t.object({
|
|
443
|
+
project: t.string(),
|
|
444
|
+
days: t.number().int().min(1).optional().default(30).describe("Minimum age in days since last update")
|
|
445
|
+
}),
|
|
446
|
+
annotations: { readOnlyHint: !0 }
|
|
447
|
+
}, async ({ project: e, days: t }) => c(async () => {
|
|
448
|
+
let r = `/api/v1/orgs/${i(n.org)}/projects/${i(e)}/flags`, a = Date.now() - t * 24 * 60 * 60 * 1e3, s = [], c;
|
|
449
|
+
do {
|
|
450
|
+
let e = await n.api.request("GET", r, { query: {
|
|
451
|
+
archived: !1,
|
|
452
|
+
limit: 100,
|
|
453
|
+
cursor: c
|
|
454
|
+
} });
|
|
455
|
+
s.push(...(e.items ?? []).filter((e) => !!e.key && !!e.name && !!e.updatedAt && new Date(e.updatedAt).getTime() < a)), c = e.next_cursor ?? void 0;
|
|
456
|
+
} while (c);
|
|
457
|
+
let l = s.length > x, u = s.slice(0, x), d = [];
|
|
458
|
+
for (let e of u) {
|
|
459
|
+
let t = await n.api.request("GET", `${r}/${i(e.key)}/environments`);
|
|
460
|
+
if (t.length === 0) continue;
|
|
461
|
+
let a = t.every((e) => e.isEnabled), o = t.every((e) => !e.isEnabled);
|
|
462
|
+
!a && !o || d.push({
|
|
463
|
+
key: e.key,
|
|
464
|
+
name: e.name,
|
|
465
|
+
updatedAt: e.updatedAt,
|
|
466
|
+
reason: a ? "enabled-everywhere" : "disabled-everywhere",
|
|
467
|
+
environments: t.length
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
return o({
|
|
471
|
+
project: e,
|
|
472
|
+
olderThanDays: t,
|
|
473
|
+
checked: u.length,
|
|
474
|
+
truncated: l,
|
|
475
|
+
...l ? { note: `Only the first ${x} of ${s.length} candidates were checked.` } : {},
|
|
476
|
+
stale: d
|
|
477
|
+
});
|
|
478
|
+
}));
|
|
479
|
+
}
|
|
480
|
+
//#endregion
|
|
481
|
+
//#region src/tools/workflows/snippets.ts
|
|
482
|
+
var C = [
|
|
483
|
+
"js",
|
|
484
|
+
"node",
|
|
485
|
+
"browser",
|
|
486
|
+
"react",
|
|
487
|
+
"python",
|
|
488
|
+
"csharp",
|
|
489
|
+
"java",
|
|
490
|
+
"go",
|
|
491
|
+
"php",
|
|
492
|
+
"ruby",
|
|
493
|
+
"swift"
|
|
494
|
+
], w = "Uses a SERVER-side SDK key (Featureflip dashboard → environment → SDK keys).", T = "Uses a CLIENT-side SDK key (safe to ship to browsers/apps).";
|
|
495
|
+
function E(e, t) {
|
|
496
|
+
switch (e) {
|
|
497
|
+
case "js": return {
|
|
498
|
+
install: "npm install @featureflip/js",
|
|
499
|
+
code: `import { FeatureflipClient, createNodePlatform } from '@featureflip/js';
|
|
500
|
+
|
|
501
|
+
const client = FeatureflipClient.get(
|
|
502
|
+
{ sdkKey: process.env.FEATUREFLIP_SDK_KEY!, baseUrl: 'https://eval.featureflip.io' },
|
|
503
|
+
createNodePlatform(),
|
|
504
|
+
);
|
|
505
|
+
await client.waitForInitialization();
|
|
506
|
+
const enabled = client.boolVariation('${t}', { user_id: userId }, false);
|
|
507
|
+
if (enabled) {
|
|
508
|
+
// new code path
|
|
509
|
+
}`,
|
|
510
|
+
notes: w
|
|
511
|
+
};
|
|
512
|
+
case "node": return {
|
|
513
|
+
install: "npm install @featureflip/node",
|
|
514
|
+
code: `import { FeatureflipClient } from '@featureflip/node';
|
|
515
|
+
|
|
516
|
+
const client = await FeatureflipClient.create({ sdkKey: process.env.FEATUREFLIP_SDK_KEY! });
|
|
517
|
+
const enabled = client.boolVariation('${t}', { user_id: userId }, false);
|
|
518
|
+
if (enabled) {
|
|
519
|
+
// new code path
|
|
520
|
+
}`,
|
|
521
|
+
notes: w
|
|
522
|
+
};
|
|
523
|
+
case "browser": return {
|
|
524
|
+
install: "npm install @featureflip/browser",
|
|
525
|
+
code: `import { FeatureflipClient } from '@featureflip/browser';
|
|
526
|
+
|
|
527
|
+
const client = FeatureflipClient.get({ clientKey: 'YOUR_CLIENT_SDK_KEY' });
|
|
528
|
+
await client.initialize();
|
|
529
|
+
const enabled = client.boolVariation('${t}', false);
|
|
530
|
+
if (enabled) {
|
|
531
|
+
// new code path
|
|
532
|
+
}`,
|
|
533
|
+
notes: T
|
|
534
|
+
};
|
|
535
|
+
case "react": return {
|
|
536
|
+
install: "npm install @featureflip/react @featureflip/browser",
|
|
537
|
+
code: `import { FeatureflipProvider, useFeatureFlag } from '@featureflip/react';
|
|
538
|
+
|
|
539
|
+
// In your app root:
|
|
540
|
+
// <FeatureflipProvider clientKey="YOUR_CLIENT_SDK_KEY">...</FeatureflipProvider>
|
|
541
|
+
|
|
542
|
+
function MyComponent() {
|
|
543
|
+
const enabled = useFeatureFlag('${t}', false);
|
|
544
|
+
return enabled ? <NewExperience /> : <CurrentExperience />;
|
|
545
|
+
}`,
|
|
546
|
+
notes: T
|
|
547
|
+
};
|
|
548
|
+
case "python": return {
|
|
549
|
+
install: "pip install featureflip",
|
|
550
|
+
code: `from featureflip import FeatureflipClient
|
|
551
|
+
|
|
552
|
+
client = FeatureflipClient(sdk_key=os.environ["FEATUREFLIP_SDK_KEY"])
|
|
553
|
+
enabled = client.variation("${t}", {"user_id": user_id}, default=False)
|
|
554
|
+
if enabled:
|
|
555
|
+
# new code path
|
|
556
|
+
...`,
|
|
557
|
+
notes: w
|
|
558
|
+
};
|
|
559
|
+
case "csharp": return {
|
|
560
|
+
install: "dotnet add package Featureflip.Client",
|
|
561
|
+
code: `using Featureflip.Client;
|
|
562
|
+
|
|
563
|
+
var client = FeatureflipClient.Get(Environment.GetEnvironmentVariable("FEATUREFLIP_SDK_KEY")!);
|
|
564
|
+
var enabled = client.BoolVariation("${t}",
|
|
565
|
+
new EvaluationContext { UserId = userId }, false);
|
|
566
|
+
if (enabled)
|
|
567
|
+
{
|
|
568
|
+
// new code path
|
|
569
|
+
}`,
|
|
570
|
+
notes: w
|
|
571
|
+
};
|
|
572
|
+
case "java": return {
|
|
573
|
+
install: "implementation 'io.featureflip:featureflip-java:2.0.0'",
|
|
574
|
+
code: `import io.featureflip.client.FeatureflipClient;
|
|
575
|
+
import io.featureflip.client.EvaluationContext;
|
|
576
|
+
|
|
577
|
+
FeatureflipClient client = FeatureflipClient.get(System.getenv("FEATUREFLIP_SDK_KEY"));
|
|
578
|
+
client.waitForInitialization();
|
|
579
|
+
boolean enabled = client.boolVariation("${t}",
|
|
580
|
+
EvaluationContext.of(userId), false);
|
|
581
|
+
if (enabled) {
|
|
582
|
+
// new code path
|
|
583
|
+
}`,
|
|
584
|
+
notes: w
|
|
585
|
+
};
|
|
586
|
+
case "go": return {
|
|
587
|
+
install: "go get github.com/canopy-labs/featureflip-go",
|
|
588
|
+
code: `import featureflip "github.com/canopy-labs/featureflip-go"
|
|
589
|
+
|
|
590
|
+
client, err := featureflip.Get(os.Getenv("FEATUREFLIP_SDK_KEY"))
|
|
591
|
+
if err != nil {
|
|
592
|
+
log.Fatal(err)
|
|
593
|
+
}
|
|
594
|
+
defer client.Close()
|
|
595
|
+
|
|
596
|
+
ctx := featureflip.EvaluationContext{UserID: userID}
|
|
597
|
+
if client.BoolVariation("${t}", ctx, false) {
|
|
598
|
+
// new code path
|
|
599
|
+
}`,
|
|
600
|
+
notes: w
|
|
601
|
+
};
|
|
602
|
+
case "php": return {
|
|
603
|
+
install: "composer require featureflip/featureflip-php",
|
|
604
|
+
code: `use Featureflip\\FeatureflipClient;
|
|
605
|
+
use Featureflip\\Config;
|
|
606
|
+
|
|
607
|
+
$client = FeatureflipClient::get(getenv('FEATUREFLIP_SDK_KEY'), new Config(
|
|
608
|
+
cache: $psrCache, httpClient: $psrHttpClient,
|
|
609
|
+
requestFactory: $psrRequestFactory, streamFactory: $psrStreamFactory,
|
|
610
|
+
));
|
|
611
|
+
$enabled = $client->boolVariation('${t}', ['user_id' => $userId], false);
|
|
612
|
+
if ($enabled) {
|
|
613
|
+
// new code path
|
|
614
|
+
}`,
|
|
615
|
+
notes: "Uses a SERVER-side SDK key (Featureflip dashboard → environment → SDK keys). Requires PSR-16 cache and PSR-18 HTTP implementations."
|
|
616
|
+
};
|
|
617
|
+
case "ruby": return {
|
|
618
|
+
install: "gem \"featureflip\"",
|
|
619
|
+
code: `require "featureflip"
|
|
620
|
+
|
|
621
|
+
client = Featureflip::Client.get(ENV.fetch("FEATUREFLIP_SDK_KEY"))
|
|
622
|
+
enabled = client.bool_variation("${t}", { "user_id" => user_id }, false)
|
|
623
|
+
if enabled
|
|
624
|
+
# new code path
|
|
625
|
+
end`,
|
|
626
|
+
notes: w
|
|
627
|
+
};
|
|
628
|
+
case "swift": return {
|
|
629
|
+
install: ".package(url: \"https://github.com/canopy-labs/featureflip-swift\", from: \"2.0.0\")",
|
|
630
|
+
code: `import Featureflip
|
|
631
|
+
|
|
632
|
+
let config = FeatureflipConfig(clientKey: "YOUR_CLIENT_SDK_KEY")
|
|
633
|
+
let client = FeatureflipClient(config: config)
|
|
634
|
+
await client.initialize()
|
|
635
|
+
let enabled = client.boolVariation("${t}", default: false)
|
|
636
|
+
if enabled {
|
|
637
|
+
// new code path
|
|
638
|
+
}`,
|
|
639
|
+
notes: T
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
//#endregion
|
|
644
|
+
//#region src/tools/workflows/wrap-feature.ts
|
|
645
|
+
function D(e, n) {
|
|
646
|
+
e.registerTool("wrap_feature", {
|
|
647
|
+
title: "Wrap a feature in a new flag",
|
|
648
|
+
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.",
|
|
649
|
+
inputSchema: t.object({
|
|
650
|
+
project: t.string().describe("Project key"),
|
|
651
|
+
key: t.string().describe("Flag key, e.g. new-checkout"),
|
|
652
|
+
name: t.string().optional().describe("Display name; defaults to the key"),
|
|
653
|
+
description: t.string().optional().describe("What this flag guards"),
|
|
654
|
+
tags: t.array(t.string()).optional(),
|
|
655
|
+
language: t.enum(C).describe("SDK language of the codebase being edited"),
|
|
656
|
+
idempotency_key: t.string().optional()
|
|
657
|
+
}),
|
|
658
|
+
annotations: {}
|
|
659
|
+
}, async ({ project: e, key: t, name: r, description: a, tags: s, language: l, idempotency_key: u }) => c(async () => {
|
|
660
|
+
let c = await n.api.request("POST", `/api/v1/orgs/${i(n.org)}/projects/${i(e)}/flags`, {
|
|
661
|
+
body: {
|
|
662
|
+
key: t,
|
|
663
|
+
name: r ?? t,
|
|
664
|
+
type: "Boolean",
|
|
665
|
+
description: a,
|
|
666
|
+
tags: s
|
|
667
|
+
},
|
|
668
|
+
idempotencyKey: u
|
|
669
|
+
}), d = E(l, t);
|
|
670
|
+
return o({
|
|
671
|
+
flag: c,
|
|
672
|
+
integration: {
|
|
673
|
+
language: l,
|
|
674
|
+
install: d.install,
|
|
675
|
+
code: d.code,
|
|
676
|
+
notes: d.notes
|
|
677
|
+
},
|
|
678
|
+
next_steps: [
|
|
679
|
+
"Apply the snippet around the new code path.",
|
|
680
|
+
`Enable in a non-production environment first: toggle_flag { project: "${e}", flag: "${t}", environment: "<env>", enabled: true }.`,
|
|
681
|
+
"Ramp with update_targeting (rolloutPercentage) when ready."
|
|
682
|
+
]
|
|
683
|
+
});
|
|
684
|
+
}));
|
|
685
|
+
}
|
|
686
|
+
//#endregion
|
|
687
|
+
//#region src/server.ts
|
|
688
|
+
function O(t) {
|
|
689
|
+
let r = new e({
|
|
690
|
+
name: "featureflip",
|
|
691
|
+
version: n
|
|
692
|
+
});
|
|
693
|
+
return u(r, t), d(r, t), f(r, t), m(r, t), g(r, t), y(r, t), b(r, t), S(r, t), D(r, t), r;
|
|
694
|
+
}
|
|
695
|
+
//#endregion
|
|
696
|
+
//#region src/config.ts
|
|
697
|
+
function k(e = process.env) {
|
|
698
|
+
let t = e.FEATUREFLIP_TOKEN?.trim();
|
|
699
|
+
if (!t) throw Error("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.");
|
|
700
|
+
return {
|
|
701
|
+
token: t,
|
|
702
|
+
baseUrl: (e.FEATUREFLIP_API_URL?.trim() || "https://api.featureflip.io").replace(/\/+$/, ""),
|
|
703
|
+
org: e.FEATUREFLIP_ORG?.trim() || void 0
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
//#endregion
|
|
707
|
+
//#region src/org.ts
|
|
708
|
+
async function A(e, t) {
|
|
709
|
+
if (t) return t;
|
|
710
|
+
let n = (await e.request("GET", "/api/v1/orgs", { query: { limit: 100 } })).items ?? [];
|
|
711
|
+
if (n.length === 1) {
|
|
712
|
+
let e = n[0].slug;
|
|
713
|
+
if (!e) throw Error("The token can see one organization, but it has no slug set.");
|
|
714
|
+
return e;
|
|
715
|
+
}
|
|
716
|
+
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(", "));
|
|
717
|
+
}
|
|
718
|
+
//#endregion
|
|
719
|
+
export { a, r as i, k as n, n as o, O as r, A as t };
|