@frontmcp/plugin-feature-flags 0.0.1
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 +201 -0
- package/adapters/feature-flag-adapter.interface.d.ts +18 -0
- package/adapters/feature-flag-adapter.interface.d.ts.map +1 -0
- package/adapters/index.d.ts +9 -0
- package/adapters/index.d.ts.map +1 -0
- package/adapters/launchdarkly.adapter.d.ts +21 -0
- package/adapters/launchdarkly.adapter.d.ts.map +1 -0
- package/adapters/splitio.adapter.d.ts +21 -0
- package/adapters/splitio.adapter.d.ts.map +1 -0
- package/adapters/static.adapter.d.ts +16 -0
- package/adapters/static.adapter.d.ts.map +1 -0
- package/adapters/unleash.adapter.d.ts +23 -0
- package/adapters/unleash.adapter.d.ts.map +1 -0
- package/esm/index.mjs +634 -0
- package/esm/package.json +72 -0
- package/feature-flag.context-extension.d.ts +45 -0
- package/feature-flag.context-extension.d.ts.map +1 -0
- package/feature-flag.plugin.d.ts +67 -0
- package/feature-flag.plugin.d.ts.map +1 -0
- package/feature-flag.symbols.d.ts +17 -0
- package/feature-flag.symbols.d.ts.map +1 -0
- package/feature-flag.types.d.ts +88 -0
- package/feature-flag.types.d.ts.map +1 -0
- package/index.d.ts +34 -0
- package/index.d.ts.map +1 -0
- package/index.js +638 -0
- package/package.json +72 -0
- package/providers/feature-flag-accessor.provider.d.ts +39 -0
- package/providers/feature-flag-accessor.provider.d.ts.map +1 -0
- package/providers/index.d.ts +2 -0
- package/providers/index.d.ts.map +1 -0
package/index.js
ADDED
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
7
|
+
var __esm = (fn, res) => function __init() {
|
|
8
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
9
|
+
};
|
|
10
|
+
var __export = (target, all) => {
|
|
11
|
+
for (var name in all)
|
|
12
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
13
|
+
};
|
|
14
|
+
var __copyProps = (to, from, except, desc) => {
|
|
15
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
16
|
+
for (let key of __getOwnPropNames(from))
|
|
17
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
18
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
19
|
+
}
|
|
20
|
+
return to;
|
|
21
|
+
};
|
|
22
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
23
|
+
var __decorateClass = (decorators, target, key, kind) => {
|
|
24
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
25
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--)
|
|
26
|
+
if (decorator = decorators[i])
|
|
27
|
+
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
28
|
+
if (kind && result) __defProp(target, key, result);
|
|
29
|
+
return result;
|
|
30
|
+
};
|
|
31
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
32
|
+
|
|
33
|
+
// plugins/plugin-feature-flags/src/adapters/splitio.adapter.ts
|
|
34
|
+
var splitio_adapter_exports = {};
|
|
35
|
+
__export(splitio_adapter_exports, {
|
|
36
|
+
SplitioFeatureFlagAdapter: () => SplitioFeatureFlagAdapter
|
|
37
|
+
});
|
|
38
|
+
var SplitioFeatureFlagAdapter;
|
|
39
|
+
var init_splitio_adapter = __esm({
|
|
40
|
+
"plugins/plugin-feature-flags/src/adapters/splitio.adapter.ts"() {
|
|
41
|
+
"use strict";
|
|
42
|
+
SplitioFeatureFlagAdapter = class {
|
|
43
|
+
config;
|
|
44
|
+
factory;
|
|
45
|
+
client;
|
|
46
|
+
constructor(config) {
|
|
47
|
+
this.config = config;
|
|
48
|
+
}
|
|
49
|
+
async initialize() {
|
|
50
|
+
let SplitFactory;
|
|
51
|
+
try {
|
|
52
|
+
({ SplitFactory } = require("@splitsoftware/splitio"));
|
|
53
|
+
} catch {
|
|
54
|
+
throw new Error("Split.io SDK not found. Install it: npm install @splitsoftware/splitio");
|
|
55
|
+
}
|
|
56
|
+
this.factory = SplitFactory({
|
|
57
|
+
core: {
|
|
58
|
+
authorizationKey: this.config.apiKey
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
this.client = this.factory.client();
|
|
62
|
+
await this.client.ready();
|
|
63
|
+
}
|
|
64
|
+
async isEnabled(flagKey, context) {
|
|
65
|
+
if (!this.client) throw new Error("SplitioFeatureFlagAdapter not initialized");
|
|
66
|
+
const key = context.userId ?? context.sessionId ?? "anonymous";
|
|
67
|
+
const treatment = this.client.getTreatment(key, flagKey, context.attributes);
|
|
68
|
+
return treatment === "on";
|
|
69
|
+
}
|
|
70
|
+
async getVariant(flagKey, context) {
|
|
71
|
+
if (!this.client) throw new Error("SplitioFeatureFlagAdapter not initialized");
|
|
72
|
+
const key = context.userId ?? context.sessionId ?? "anonymous";
|
|
73
|
+
const treatment = this.client.getTreatment(key, flagKey, context.attributes);
|
|
74
|
+
return {
|
|
75
|
+
name: treatment,
|
|
76
|
+
value: treatment,
|
|
77
|
+
enabled: treatment === "on"
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
async evaluateFlags(flagKeys, context) {
|
|
81
|
+
const results = /* @__PURE__ */ new Map();
|
|
82
|
+
for (const key of flagKeys) {
|
|
83
|
+
results.set(key, await this.isEnabled(key, context));
|
|
84
|
+
}
|
|
85
|
+
return results;
|
|
86
|
+
}
|
|
87
|
+
async destroy() {
|
|
88
|
+
if (this.client) {
|
|
89
|
+
await this.client.destroy();
|
|
90
|
+
}
|
|
91
|
+
this.client = void 0;
|
|
92
|
+
this.factory = void 0;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// plugins/plugin-feature-flags/src/adapters/launchdarkly.adapter.ts
|
|
99
|
+
var launchdarkly_adapter_exports = {};
|
|
100
|
+
__export(launchdarkly_adapter_exports, {
|
|
101
|
+
LaunchDarklyFeatureFlagAdapter: () => LaunchDarklyFeatureFlagAdapter
|
|
102
|
+
});
|
|
103
|
+
var LaunchDarklyFeatureFlagAdapter;
|
|
104
|
+
var init_launchdarkly_adapter = __esm({
|
|
105
|
+
"plugins/plugin-feature-flags/src/adapters/launchdarkly.adapter.ts"() {
|
|
106
|
+
"use strict";
|
|
107
|
+
LaunchDarklyFeatureFlagAdapter = class {
|
|
108
|
+
config;
|
|
109
|
+
client;
|
|
110
|
+
constructor(config) {
|
|
111
|
+
this.config = config;
|
|
112
|
+
}
|
|
113
|
+
async initialize() {
|
|
114
|
+
let init;
|
|
115
|
+
try {
|
|
116
|
+
({ init } = require("@launchdarkly/node-server-sdk"));
|
|
117
|
+
} catch {
|
|
118
|
+
throw new Error("LaunchDarkly SDK not found. Install it: npm install @launchdarkly/node-server-sdk");
|
|
119
|
+
}
|
|
120
|
+
this.client = init(this.config.sdkKey);
|
|
121
|
+
await this.client.waitForInitialization();
|
|
122
|
+
}
|
|
123
|
+
buildLDContext(context) {
|
|
124
|
+
return {
|
|
125
|
+
kind: "user",
|
|
126
|
+
key: context.userId ?? context.sessionId ?? "anonymous",
|
|
127
|
+
...context.attributes ?? {}
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
async isEnabled(flagKey, context) {
|
|
131
|
+
if (!this.client) throw new Error("LaunchDarklyFeatureFlagAdapter not initialized");
|
|
132
|
+
const ldContext = this.buildLDContext(context);
|
|
133
|
+
return this.client.variation(flagKey, ldContext, false);
|
|
134
|
+
}
|
|
135
|
+
async getVariant(flagKey, context) {
|
|
136
|
+
if (!this.client) throw new Error("LaunchDarklyFeatureFlagAdapter not initialized");
|
|
137
|
+
const ldContext = this.buildLDContext(context);
|
|
138
|
+
const detail = await this.client.variationDetail(flagKey, ldContext, false);
|
|
139
|
+
const value = detail.value;
|
|
140
|
+
return {
|
|
141
|
+
name: String(value),
|
|
142
|
+
value,
|
|
143
|
+
enabled: Boolean(value)
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
async evaluateFlags(flagKeys, context) {
|
|
147
|
+
const results = /* @__PURE__ */ new Map();
|
|
148
|
+
for (const key of flagKeys) {
|
|
149
|
+
results.set(key, await this.isEnabled(key, context));
|
|
150
|
+
}
|
|
151
|
+
return results;
|
|
152
|
+
}
|
|
153
|
+
async destroy() {
|
|
154
|
+
if (this.client) {
|
|
155
|
+
await this.client.close();
|
|
156
|
+
}
|
|
157
|
+
this.client = void 0;
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// plugins/plugin-feature-flags/src/adapters/unleash.adapter.ts
|
|
164
|
+
var unleash_adapter_exports = {};
|
|
165
|
+
__export(unleash_adapter_exports, {
|
|
166
|
+
UnleashFeatureFlagAdapter: () => UnleashFeatureFlagAdapter
|
|
167
|
+
});
|
|
168
|
+
var UnleashFeatureFlagAdapter;
|
|
169
|
+
var init_unleash_adapter = __esm({
|
|
170
|
+
"plugins/plugin-feature-flags/src/adapters/unleash.adapter.ts"() {
|
|
171
|
+
"use strict";
|
|
172
|
+
UnleashFeatureFlagAdapter = class {
|
|
173
|
+
config;
|
|
174
|
+
client;
|
|
175
|
+
constructor(config) {
|
|
176
|
+
this.config = config;
|
|
177
|
+
}
|
|
178
|
+
async initialize() {
|
|
179
|
+
let Unleash;
|
|
180
|
+
try {
|
|
181
|
+
({ Unleash } = require("unleash-client"));
|
|
182
|
+
} catch {
|
|
183
|
+
throw new Error("Unleash SDK not found. Install it: npm install unleash-client");
|
|
184
|
+
}
|
|
185
|
+
const options = {
|
|
186
|
+
url: this.config.url,
|
|
187
|
+
appName: this.config.appName
|
|
188
|
+
};
|
|
189
|
+
if (this.config.apiKey) {
|
|
190
|
+
options["customHeaders"] = { Authorization: this.config.apiKey };
|
|
191
|
+
}
|
|
192
|
+
this.client = new Unleash(options);
|
|
193
|
+
await this.client.start();
|
|
194
|
+
}
|
|
195
|
+
buildUnleashContext(context) {
|
|
196
|
+
return {
|
|
197
|
+
userId: context.userId,
|
|
198
|
+
sessionId: context.sessionId,
|
|
199
|
+
properties: context.attributes ?? {}
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
async isEnabled(flagKey, context) {
|
|
203
|
+
if (!this.client) throw new Error("UnleashFeatureFlagAdapter not initialized");
|
|
204
|
+
const unleashCtx = this.buildUnleashContext(context);
|
|
205
|
+
return this.client.isEnabled(flagKey, unleashCtx);
|
|
206
|
+
}
|
|
207
|
+
async getVariant(flagKey, context) {
|
|
208
|
+
if (!this.client) throw new Error("UnleashFeatureFlagAdapter not initialized");
|
|
209
|
+
const unleashCtx = this.buildUnleashContext(context);
|
|
210
|
+
const variant = this.client.getVariant(flagKey, unleashCtx);
|
|
211
|
+
return {
|
|
212
|
+
name: variant.name ?? "disabled",
|
|
213
|
+
value: variant.payload?.value ?? variant.name,
|
|
214
|
+
enabled: variant.enabled ?? false
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
async evaluateFlags(flagKeys, context) {
|
|
218
|
+
const results = /* @__PURE__ */ new Map();
|
|
219
|
+
for (const key of flagKeys) {
|
|
220
|
+
results.set(key, await this.isEnabled(key, context));
|
|
221
|
+
}
|
|
222
|
+
return results;
|
|
223
|
+
}
|
|
224
|
+
async destroy() {
|
|
225
|
+
if (this.client) {
|
|
226
|
+
this.client.destroy();
|
|
227
|
+
}
|
|
228
|
+
this.client = void 0;
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// plugins/plugin-feature-flags/src/index.ts
|
|
235
|
+
var index_exports = {};
|
|
236
|
+
__export(index_exports, {
|
|
237
|
+
FeatureFlagAccessor: () => FeatureFlagAccessor,
|
|
238
|
+
FeatureFlagAccessorToken: () => FeatureFlagAccessorToken,
|
|
239
|
+
FeatureFlagAdapterToken: () => FeatureFlagAdapterToken,
|
|
240
|
+
FeatureFlagConfigToken: () => FeatureFlagConfigToken,
|
|
241
|
+
FeatureFlagPlugin: () => FeatureFlagPlugin,
|
|
242
|
+
LaunchDarklyFeatureFlagAdapter: () => LaunchDarklyFeatureFlagAdapter,
|
|
243
|
+
SplitioFeatureFlagAdapter: () => SplitioFeatureFlagAdapter,
|
|
244
|
+
StaticFeatureFlagAdapter: () => StaticFeatureFlagAdapter,
|
|
245
|
+
UnleashFeatureFlagAdapter: () => UnleashFeatureFlagAdapter,
|
|
246
|
+
default: () => FeatureFlagPlugin,
|
|
247
|
+
getFeatureFlags: () => getFeatureFlags,
|
|
248
|
+
tryGetFeatureFlags: () => tryGetFeatureFlags
|
|
249
|
+
});
|
|
250
|
+
module.exports = __toCommonJS(index_exports);
|
|
251
|
+
|
|
252
|
+
// plugins/plugin-feature-flags/src/feature-flag.plugin.ts
|
|
253
|
+
var import_sdk2 = require("@frontmcp/sdk");
|
|
254
|
+
|
|
255
|
+
// plugins/plugin-feature-flags/src/feature-flag.symbols.ts
|
|
256
|
+
var FeatureFlagAdapterToken = /* @__PURE__ */ Symbol(
|
|
257
|
+
"plugin:feature-flags:adapter"
|
|
258
|
+
);
|
|
259
|
+
var FeatureFlagConfigToken = /* @__PURE__ */ Symbol(
|
|
260
|
+
"plugin:feature-flags:config"
|
|
261
|
+
);
|
|
262
|
+
var FeatureFlagAccessorToken = /* @__PURE__ */ Symbol(
|
|
263
|
+
"plugin:feature-flags:accessor"
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
// plugins/plugin-feature-flags/src/adapters/static.adapter.ts
|
|
267
|
+
var StaticFeatureFlagAdapter = class {
|
|
268
|
+
flags;
|
|
269
|
+
constructor(flags) {
|
|
270
|
+
this.flags = { ...flags };
|
|
271
|
+
}
|
|
272
|
+
async initialize() {
|
|
273
|
+
}
|
|
274
|
+
async isEnabled(flagKey, _context) {
|
|
275
|
+
const flag = this.flags[flagKey];
|
|
276
|
+
if (flag === void 0) return false;
|
|
277
|
+
if (typeof flag === "boolean") return flag;
|
|
278
|
+
return flag.enabled;
|
|
279
|
+
}
|
|
280
|
+
async getVariant(flagKey, _context) {
|
|
281
|
+
const flag = this.flags[flagKey];
|
|
282
|
+
if (flag === void 0) {
|
|
283
|
+
return { name: "off", value: void 0, enabled: false };
|
|
284
|
+
}
|
|
285
|
+
if (typeof flag === "boolean") {
|
|
286
|
+
return { name: flag ? "on" : "off", value: flag, enabled: flag };
|
|
287
|
+
}
|
|
288
|
+
return { ...flag };
|
|
289
|
+
}
|
|
290
|
+
async evaluateFlags(flagKeys, context) {
|
|
291
|
+
const results = /* @__PURE__ */ new Map();
|
|
292
|
+
for (const key of flagKeys) {
|
|
293
|
+
results.set(key, await this.isEnabled(key, context));
|
|
294
|
+
}
|
|
295
|
+
return results;
|
|
296
|
+
}
|
|
297
|
+
async destroy() {
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
// plugins/plugin-feature-flags/src/providers/feature-flag-accessor.provider.ts
|
|
302
|
+
var import_sdk = require("@frontmcp/sdk");
|
|
303
|
+
var FeatureFlagAccessor = class {
|
|
304
|
+
adapter;
|
|
305
|
+
ctx;
|
|
306
|
+
config;
|
|
307
|
+
cache = /* @__PURE__ */ new Map();
|
|
308
|
+
constructor(adapter, ctx, config) {
|
|
309
|
+
this.adapter = adapter;
|
|
310
|
+
this.ctx = ctx;
|
|
311
|
+
this.config = config;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Check if a feature flag is enabled.
|
|
315
|
+
*/
|
|
316
|
+
async isEnabled(flagKey, defaultValue) {
|
|
317
|
+
const cacheStrategy = this.config.cacheStrategy ?? "none";
|
|
318
|
+
const cacheTtlMs = this.config.cacheTtlMs ?? 3e4;
|
|
319
|
+
if (cacheStrategy !== "none") {
|
|
320
|
+
const cached = this.cache.get(flagKey);
|
|
321
|
+
if (cached && Date.now() < cached.expiresAt) {
|
|
322
|
+
return cached.value;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
const context = this.buildContext();
|
|
326
|
+
let result;
|
|
327
|
+
try {
|
|
328
|
+
result = await this.adapter.isEnabled(flagKey, context);
|
|
329
|
+
} catch {
|
|
330
|
+
result = defaultValue ?? this.config.defaultValue ?? false;
|
|
331
|
+
}
|
|
332
|
+
if (cacheStrategy !== "none") {
|
|
333
|
+
this.cache.set(flagKey, { value: result, expiresAt: Date.now() + cacheTtlMs });
|
|
334
|
+
}
|
|
335
|
+
return result;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Get a feature flag variant (for multi-variate flags).
|
|
339
|
+
*/
|
|
340
|
+
async getVariant(flagKey) {
|
|
341
|
+
const context = this.buildContext();
|
|
342
|
+
return this.adapter.getVariant(flagKey, context);
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Batch evaluate multiple flags at once.
|
|
346
|
+
*/
|
|
347
|
+
async evaluateFlags(flagKeys) {
|
|
348
|
+
const context = this.buildContext();
|
|
349
|
+
return this.adapter.evaluateFlags(flagKeys, context);
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Resolve a FeatureFlagRef (string or object) to a boolean.
|
|
353
|
+
*/
|
|
354
|
+
async resolveRef(ref) {
|
|
355
|
+
if (typeof ref === "string") {
|
|
356
|
+
return this.isEnabled(ref);
|
|
357
|
+
}
|
|
358
|
+
return this.isEnabled(ref.key, ref.defaultValue);
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Build the FeatureFlagContext from the current FrontMcpContext.
|
|
362
|
+
*/
|
|
363
|
+
buildContext() {
|
|
364
|
+
const userId = this.config.userIdResolver ? this.config.userIdResolver(this.ctx) : this.ctx.authInfo?.extra?.["sub"] ?? this.ctx.authInfo?.extra?.["userId"] ?? this.ctx.authInfo?.clientId;
|
|
365
|
+
const attributes = this.config.attributesResolver ? this.config.attributesResolver(this.ctx) : {};
|
|
366
|
+
return {
|
|
367
|
+
userId: userId ?? void 0,
|
|
368
|
+
sessionId: this.ctx.sessionId,
|
|
369
|
+
attributes
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
FeatureFlagAccessor = __decorateClass([
|
|
374
|
+
(0, import_sdk.Provider)({
|
|
375
|
+
name: "provider:feature-flags:accessor",
|
|
376
|
+
description: "Context-scoped accessor for feature flag evaluation",
|
|
377
|
+
scope: import_sdk.ProviderScope.CONTEXT
|
|
378
|
+
})
|
|
379
|
+
], FeatureFlagAccessor);
|
|
380
|
+
function createFeatureFlagAccessor(adapter, ctx, config) {
|
|
381
|
+
return new FeatureFlagAccessor(adapter, ctx, config);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// plugins/plugin-feature-flags/src/feature-flag.plugin.ts
|
|
385
|
+
var ListPromptsHook = (0, import_sdk2.FlowHooksOf)("prompts:list-prompts");
|
|
386
|
+
var SearchSkillsHook = (0, import_sdk2.FlowHooksOf)("skills:search");
|
|
387
|
+
var FeatureFlagPlugin = class extends import_sdk2.DynamicPlugin {
|
|
388
|
+
options;
|
|
389
|
+
constructor(options) {
|
|
390
|
+
super();
|
|
391
|
+
this.options = options;
|
|
392
|
+
}
|
|
393
|
+
async filterListTools(flowCtx) {
|
|
394
|
+
const { tools } = flowCtx.state;
|
|
395
|
+
if (!tools || tools.length === 0) return;
|
|
396
|
+
const flaggedTools = this.collectFlagRefs(tools, (item) => item.tool.metadata?.featureFlag);
|
|
397
|
+
if (flaggedTools.size === 0) return;
|
|
398
|
+
const adapter = this.get(FeatureFlagAdapterToken);
|
|
399
|
+
const flagResults = await this.batchEvaluateRefs(adapter, flaggedTools);
|
|
400
|
+
const filtered = tools.filter((item) => {
|
|
401
|
+
const ref = item.tool.metadata?.featureFlag;
|
|
402
|
+
if (!ref) return true;
|
|
403
|
+
return this.isRefEnabled(ref, flagResults);
|
|
404
|
+
});
|
|
405
|
+
flowCtx.state.set("tools", filtered);
|
|
406
|
+
}
|
|
407
|
+
async filterListResources(flowCtx) {
|
|
408
|
+
const { resources } = flowCtx.state;
|
|
409
|
+
if (!resources || resources.length === 0) return;
|
|
410
|
+
const flaggedResources = this.collectFlagRefs(resources, (item) => item.resource.metadata?.featureFlag);
|
|
411
|
+
if (flaggedResources.size === 0) return;
|
|
412
|
+
const adapter = this.get(FeatureFlagAdapterToken);
|
|
413
|
+
const flagResults = await this.batchEvaluateRefs(adapter, flaggedResources);
|
|
414
|
+
const filtered = resources.filter((item) => {
|
|
415
|
+
const ref = item.resource.metadata?.featureFlag;
|
|
416
|
+
if (!ref) return true;
|
|
417
|
+
return this.isRefEnabled(ref, flagResults);
|
|
418
|
+
});
|
|
419
|
+
flowCtx.state.set("resources", filtered);
|
|
420
|
+
}
|
|
421
|
+
async filterListPrompts(flowCtx) {
|
|
422
|
+
const { prompts } = flowCtx.state;
|
|
423
|
+
if (!prompts || prompts.length === 0) return;
|
|
424
|
+
const flaggedPrompts = this.collectFlagRefs(prompts, (item) => item.prompt.metadata?.featureFlag);
|
|
425
|
+
if (flaggedPrompts.size === 0) return;
|
|
426
|
+
const adapter = this.get(FeatureFlagAdapterToken);
|
|
427
|
+
const flagResults = await this.batchEvaluateRefs(adapter, flaggedPrompts);
|
|
428
|
+
const filtered = prompts.filter((item) => {
|
|
429
|
+
const ref = item.prompt.metadata?.featureFlag;
|
|
430
|
+
if (!ref) return true;
|
|
431
|
+
return this.isRefEnabled(ref, flagResults);
|
|
432
|
+
});
|
|
433
|
+
flowCtx.state.set("prompts", filtered);
|
|
434
|
+
}
|
|
435
|
+
async filterSearchSkills(flowCtx) {
|
|
436
|
+
const { results } = flowCtx.state;
|
|
437
|
+
if (!results || results.length === 0) return;
|
|
438
|
+
const flaggedSkills = this.collectFlagRefs(results, (item) => item.metadata?.featureFlag);
|
|
439
|
+
if (flaggedSkills.size === 0) return;
|
|
440
|
+
const adapter = this.get(FeatureFlagAdapterToken);
|
|
441
|
+
const flagResults = await this.batchEvaluateRefs(adapter, flaggedSkills);
|
|
442
|
+
const filtered = results.filter((item) => {
|
|
443
|
+
const ref = item.metadata?.featureFlag;
|
|
444
|
+
if (!ref) return true;
|
|
445
|
+
return this.isRefEnabled(ref, flagResults);
|
|
446
|
+
});
|
|
447
|
+
flowCtx.state.set("results", filtered);
|
|
448
|
+
}
|
|
449
|
+
async gateToolExecution(flowCtx) {
|
|
450
|
+
const { tool } = flowCtx.state;
|
|
451
|
+
if (!tool) return;
|
|
452
|
+
const ref = tool.metadata?.featureFlag;
|
|
453
|
+
if (!ref) return;
|
|
454
|
+
const adapter = this.get(FeatureFlagAdapterToken);
|
|
455
|
+
const key = typeof ref === "string" ? ref : ref.key;
|
|
456
|
+
const defaultValue = typeof ref === "object" ? ref.defaultValue ?? false : false;
|
|
457
|
+
let enabled;
|
|
458
|
+
try {
|
|
459
|
+
enabled = await adapter.isEnabled(key, {});
|
|
460
|
+
} catch {
|
|
461
|
+
enabled = defaultValue;
|
|
462
|
+
}
|
|
463
|
+
if (!enabled) {
|
|
464
|
+
throw new Error(`Tool "${tool.metadata.name}" is disabled by feature flag "${key}"`);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
468
|
+
// Private Helpers
|
|
469
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
470
|
+
/**
|
|
471
|
+
* Collect unique flag keys from items that have a featureFlag metadata.
|
|
472
|
+
*/
|
|
473
|
+
collectFlagRefs(items, getRef) {
|
|
474
|
+
const refs = /* @__PURE__ */ new Map();
|
|
475
|
+
for (const item of items) {
|
|
476
|
+
const ref = getRef(item);
|
|
477
|
+
if (ref) {
|
|
478
|
+
const key = typeof ref === "string" ? ref : ref.key;
|
|
479
|
+
if (!refs.has(key)) {
|
|
480
|
+
refs.set(key, ref);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return refs;
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Batch evaluate all collected flag refs via the adapter.
|
|
488
|
+
*/
|
|
489
|
+
async batchEvaluateRefs(adapter, refs) {
|
|
490
|
+
const keys = Array.from(refs.keys());
|
|
491
|
+
return adapter.evaluateFlags(keys, {});
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Determine if a feature flag ref is enabled given adapter results.
|
|
495
|
+
* For object-style refs, `defaultValue` acts as a fallback when the adapter
|
|
496
|
+
* returns false (i.e., the flag is unknown to the adapter).
|
|
497
|
+
*/
|
|
498
|
+
isRefEnabled(ref, flagResults) {
|
|
499
|
+
const key = typeof ref === "string" ? ref : ref.key;
|
|
500
|
+
const adapterResult = flagResults.get(key);
|
|
501
|
+
const defaultValue = typeof ref === "object" ? ref.defaultValue ?? false : false;
|
|
502
|
+
if (adapterResult === true) return true;
|
|
503
|
+
return defaultValue;
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
/**
|
|
507
|
+
* Dynamic providers based on plugin options.
|
|
508
|
+
*/
|
|
509
|
+
__publicField(FeatureFlagPlugin, "dynamicProviders", (options) => {
|
|
510
|
+
const providers = [];
|
|
511
|
+
switch (options.adapter) {
|
|
512
|
+
case "static":
|
|
513
|
+
providers.push({
|
|
514
|
+
name: "feature-flags:adapter:static",
|
|
515
|
+
provide: FeatureFlagAdapterToken,
|
|
516
|
+
useValue: new StaticFeatureFlagAdapter(options.flags)
|
|
517
|
+
});
|
|
518
|
+
break;
|
|
519
|
+
case "splitio":
|
|
520
|
+
providers.push({
|
|
521
|
+
name: "feature-flags:adapter:splitio",
|
|
522
|
+
provide: FeatureFlagAdapterToken,
|
|
523
|
+
inject: () => [],
|
|
524
|
+
useFactory: async () => {
|
|
525
|
+
const { SplitioFeatureFlagAdapter: SplitioFeatureFlagAdapter2 } = (init_splitio_adapter(), __toCommonJS(splitio_adapter_exports));
|
|
526
|
+
const adapter = new SplitioFeatureFlagAdapter2(options.config);
|
|
527
|
+
await adapter.initialize();
|
|
528
|
+
return adapter;
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
break;
|
|
532
|
+
case "launchdarkly":
|
|
533
|
+
providers.push({
|
|
534
|
+
name: "feature-flags:adapter:launchdarkly",
|
|
535
|
+
provide: FeatureFlagAdapterToken,
|
|
536
|
+
inject: () => [],
|
|
537
|
+
useFactory: async () => {
|
|
538
|
+
const { LaunchDarklyFeatureFlagAdapter: LaunchDarklyFeatureFlagAdapter2 } = (init_launchdarkly_adapter(), __toCommonJS(launchdarkly_adapter_exports));
|
|
539
|
+
const adapter = new LaunchDarklyFeatureFlagAdapter2(options.config);
|
|
540
|
+
await adapter.initialize();
|
|
541
|
+
return adapter;
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
break;
|
|
545
|
+
case "unleash":
|
|
546
|
+
providers.push({
|
|
547
|
+
name: "feature-flags:adapter:unleash",
|
|
548
|
+
provide: FeatureFlagAdapterToken,
|
|
549
|
+
inject: () => [],
|
|
550
|
+
useFactory: async () => {
|
|
551
|
+
const { UnleashFeatureFlagAdapter: UnleashFeatureFlagAdapter2 } = (init_unleash_adapter(), __toCommonJS(unleash_adapter_exports));
|
|
552
|
+
const adapter = new UnleashFeatureFlagAdapter2(options.config);
|
|
553
|
+
await adapter.initialize();
|
|
554
|
+
return adapter;
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
break;
|
|
558
|
+
case "custom":
|
|
559
|
+
providers.push({
|
|
560
|
+
name: "feature-flags:adapter:custom",
|
|
561
|
+
provide: FeatureFlagAdapterToken,
|
|
562
|
+
useValue: options.adapterInstance
|
|
563
|
+
});
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
providers.push({
|
|
567
|
+
name: "feature-flags:config",
|
|
568
|
+
provide: FeatureFlagConfigToken,
|
|
569
|
+
useValue: options
|
|
570
|
+
});
|
|
571
|
+
providers.push({
|
|
572
|
+
name: "feature-flags:accessor",
|
|
573
|
+
provide: FeatureFlagAccessorToken,
|
|
574
|
+
scope: import_sdk2.ProviderScope.CONTEXT,
|
|
575
|
+
inject: () => [FeatureFlagAdapterToken, import_sdk2.FRONTMCP_CONTEXT, FeatureFlagConfigToken],
|
|
576
|
+
useFactory: (adapter, ctx, cfg) => createFeatureFlagAccessor(adapter, ctx, cfg)
|
|
577
|
+
});
|
|
578
|
+
return providers;
|
|
579
|
+
});
|
|
580
|
+
__decorateClass([
|
|
581
|
+
import_sdk2.ListToolsHook.Did("findTools", { priority: 50 })
|
|
582
|
+
], FeatureFlagPlugin.prototype, "filterListTools", 1);
|
|
583
|
+
__decorateClass([
|
|
584
|
+
import_sdk2.ListResourcesHook.Did("findResources", { priority: 50 })
|
|
585
|
+
], FeatureFlagPlugin.prototype, "filterListResources", 1);
|
|
586
|
+
__decorateClass([
|
|
587
|
+
ListPromptsHook.Did("findPrompts", { priority: 50 })
|
|
588
|
+
], FeatureFlagPlugin.prototype, "filterListPrompts", 1);
|
|
589
|
+
__decorateClass([
|
|
590
|
+
SearchSkillsHook.Did("search", { priority: 50 })
|
|
591
|
+
], FeatureFlagPlugin.prototype, "filterSearchSkills", 1);
|
|
592
|
+
__decorateClass([
|
|
593
|
+
import_sdk2.ToolHook.Will("execute", { priority: 50 })
|
|
594
|
+
], FeatureFlagPlugin.prototype, "gateToolExecution", 1);
|
|
595
|
+
FeatureFlagPlugin = __decorateClass([
|
|
596
|
+
(0, import_sdk2.Plugin)({
|
|
597
|
+
name: "feature-flags",
|
|
598
|
+
description: "Feature flag-based capability filtering for MCP",
|
|
599
|
+
providers: [],
|
|
600
|
+
contextExtensions: [
|
|
601
|
+
{
|
|
602
|
+
property: "featureFlags",
|
|
603
|
+
token: FeatureFlagAccessorToken,
|
|
604
|
+
errorMessage: "FeatureFlagPlugin is not installed. Add FeatureFlagPlugin.init() to your plugins array."
|
|
605
|
+
}
|
|
606
|
+
]
|
|
607
|
+
})
|
|
608
|
+
], FeatureFlagPlugin);
|
|
609
|
+
|
|
610
|
+
// plugins/plugin-feature-flags/src/index.ts
|
|
611
|
+
init_splitio_adapter();
|
|
612
|
+
init_launchdarkly_adapter();
|
|
613
|
+
init_unleash_adapter();
|
|
614
|
+
|
|
615
|
+
// plugins/plugin-feature-flags/src/feature-flag.context-extension.ts
|
|
616
|
+
function getFeatureFlags(ctx) {
|
|
617
|
+
return ctx.get(FeatureFlagAccessorToken);
|
|
618
|
+
}
|
|
619
|
+
function tryGetFeatureFlags(ctx) {
|
|
620
|
+
if (typeof ctx.tryGet === "function") {
|
|
621
|
+
return ctx.tryGet(FeatureFlagAccessorToken);
|
|
622
|
+
}
|
|
623
|
+
return void 0;
|
|
624
|
+
}
|
|
625
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
626
|
+
0 && (module.exports = {
|
|
627
|
+
FeatureFlagAccessor,
|
|
628
|
+
FeatureFlagAccessorToken,
|
|
629
|
+
FeatureFlagAdapterToken,
|
|
630
|
+
FeatureFlagConfigToken,
|
|
631
|
+
FeatureFlagPlugin,
|
|
632
|
+
LaunchDarklyFeatureFlagAdapter,
|
|
633
|
+
SplitioFeatureFlagAdapter,
|
|
634
|
+
StaticFeatureFlagAdapter,
|
|
635
|
+
UnleashFeatureFlagAdapter,
|
|
636
|
+
getFeatureFlags,
|
|
637
|
+
tryGetFeatureFlags
|
|
638
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@frontmcp/plugin-feature-flags",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Feature flag plugin for FrontMCP - dynamically gate MCP capabilities behind feature flags",
|
|
5
|
+
"author": "AgentFront <info@agentfront.dev>",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"mcp",
|
|
9
|
+
"feature-flags",
|
|
10
|
+
"split",
|
|
11
|
+
"launchdarkly",
|
|
12
|
+
"unleash",
|
|
13
|
+
"plugin",
|
|
14
|
+
"frontmcp",
|
|
15
|
+
"agentfront"
|
|
16
|
+
],
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/agentfront/frontmcp.git",
|
|
20
|
+
"directory": "plugins/plugin-feature-flags"
|
|
21
|
+
},
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/agentfront/frontmcp/issues"
|
|
24
|
+
},
|
|
25
|
+
"homepage": "https://github.com/agentfront/frontmcp/blob/main/plugins/plugin-feature-flags/README.md",
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public",
|
|
28
|
+
"registry": "https://registry.npmjs.org/"
|
|
29
|
+
},
|
|
30
|
+
"type": "commonjs",
|
|
31
|
+
"main": "./index.js",
|
|
32
|
+
"module": "./esm/index.mjs",
|
|
33
|
+
"types": "./index.d.ts",
|
|
34
|
+
"sideEffects": false,
|
|
35
|
+
"exports": {
|
|
36
|
+
"./package.json": "./package.json",
|
|
37
|
+
".": {
|
|
38
|
+
"require": {
|
|
39
|
+
"types": "./index.d.ts",
|
|
40
|
+
"default": "./index.js"
|
|
41
|
+
},
|
|
42
|
+
"import": {
|
|
43
|
+
"types": "./index.d.ts",
|
|
44
|
+
"default": "./esm/index.mjs"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@frontmcp/sdk": "1.0.0",
|
|
50
|
+
"@frontmcp/utils": "1.0.0",
|
|
51
|
+
"zod": "^4.0.0"
|
|
52
|
+
},
|
|
53
|
+
"peerDependencies": {
|
|
54
|
+
"@splitsoftware/splitio": "^10.0.0 || ^11.0.0",
|
|
55
|
+
"@launchdarkly/node-server-sdk": "^9.0.0 || ^10.0.0",
|
|
56
|
+
"unleash-client": "^5.0.0 || ^6.0.0"
|
|
57
|
+
},
|
|
58
|
+
"peerDependenciesMeta": {
|
|
59
|
+
"@splitsoftware/splitio": {
|
|
60
|
+
"optional": true
|
|
61
|
+
},
|
|
62
|
+
"@launchdarkly/node-server-sdk": {
|
|
63
|
+
"optional": true
|
|
64
|
+
},
|
|
65
|
+
"unleash-client": {
|
|
66
|
+
"optional": true
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"devDependencies": {
|
|
70
|
+
"reflect-metadata": "^0.2.2"
|
|
71
|
+
}
|
|
72
|
+
}
|