@frontmcp/plugin-feature-flags 1.8.3 → 1.8.4
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/esm/index.mjs +46 -13
- package/esm/package.json +2 -2
- package/feature-flag.context.d.ts.map +1 -1
- package/feature-flag.plugin.d.ts +10 -0
- package/feature-flag.plugin.d.ts.map +1 -1
- package/index.js +43 -12
- package/package.json +2 -2
package/esm/index.mjs
CHANGED
|
@@ -247,11 +247,13 @@ import {
|
|
|
247
247
|
FlowHooksOf,
|
|
248
248
|
FRONTMCP_CONTEXT,
|
|
249
249
|
FrontMcpContextStorage,
|
|
250
|
+
isEntryGatedBy,
|
|
250
251
|
ListResourcesHook,
|
|
251
252
|
ListResourceTemplatesHook,
|
|
252
253
|
ListToolsHook,
|
|
253
254
|
Plugin,
|
|
254
255
|
ProviderScope as ProviderScope2,
|
|
256
|
+
ScopeEntry,
|
|
255
257
|
ToolHook
|
|
256
258
|
} from "@frontmcp/sdk";
|
|
257
259
|
|
|
@@ -297,14 +299,18 @@ var StaticFeatureFlagAdapter = class {
|
|
|
297
299
|
};
|
|
298
300
|
|
|
299
301
|
// plugins/plugin-feature-flags/src/feature-flag.context.ts
|
|
300
|
-
import { STATELESS_SESSION_ID } from "@frontmcp/sdk";
|
|
302
|
+
import { isAnonymousSubject, STATELESS_SESSION_ID } from "@frontmcp/sdk";
|
|
301
303
|
function verifiedSessionId(ctx) {
|
|
302
304
|
const verified = ctx.authInfo?.sessionId ?? ctx.authInfo?.extra?.["sessionId"];
|
|
303
305
|
return typeof verified === "string" && verified.length > 0 && verified !== STATELESS_SESSION_ID ? verified : void 0;
|
|
304
306
|
}
|
|
307
|
+
function signedInUserId(ctx) {
|
|
308
|
+
const userId = ctx.authInfo?.extra?.["sub"] ?? ctx.authInfo?.extra?.["userId"] ?? ctx.authInfo?.clientId;
|
|
309
|
+
return isAnonymousSubject(userId) ? void 0 : userId;
|
|
310
|
+
}
|
|
305
311
|
function buildFeatureFlagContext(ctx, config) {
|
|
306
312
|
if (!ctx) return {};
|
|
307
|
-
const userId = config.userIdResolver ? config.userIdResolver(ctx) : ctx
|
|
313
|
+
const userId = config.userIdResolver ? config.userIdResolver(ctx) : signedInUserId(ctx);
|
|
308
314
|
return {
|
|
309
315
|
userId: userId ?? void 0,
|
|
310
316
|
sessionId: verifiedSessionId(ctx),
|
|
@@ -415,13 +421,14 @@ var FeatureFlagPlugin = class extends DynamicPlugin {
|
|
|
415
421
|
async filterListTools(flowCtx) {
|
|
416
422
|
const { tools } = flowCtx.state;
|
|
417
423
|
if (!tools || tools.length === 0) return;
|
|
418
|
-
const
|
|
424
|
+
const judged = this.judgedItems(tools, (item) => ({ tool: item.tool }));
|
|
425
|
+
const flaggedTools = this.collectFlagRefs(judged, (item) => item.tool.metadata.featureFlag);
|
|
419
426
|
if (flaggedTools.size === 0) return;
|
|
420
427
|
const adapter = this.get(FeatureFlagAdapterToken);
|
|
421
428
|
const flagResults = await this.batchEvaluateRefs(adapter, flaggedTools);
|
|
422
429
|
const filtered = tools.filter((item) => {
|
|
423
430
|
const ref = item.tool.metadata?.featureFlag;
|
|
424
|
-
if (!ref) return true;
|
|
431
|
+
if (!ref || !judged.has(item)) return true;
|
|
425
432
|
return this.isRefEnabled(ref, flagResults);
|
|
426
433
|
});
|
|
427
434
|
flowCtx.state.set("tools", filtered);
|
|
@@ -429,13 +436,14 @@ var FeatureFlagPlugin = class extends DynamicPlugin {
|
|
|
429
436
|
async filterListResources(flowCtx) {
|
|
430
437
|
const { resources } = flowCtx.state;
|
|
431
438
|
if (!resources || resources.length === 0) return;
|
|
432
|
-
const
|
|
439
|
+
const judged = this.judgedItems(resources, (item) => ({ resource: item.resource }));
|
|
440
|
+
const flaggedResources = this.collectFlagRefs(judged, (item) => item.resource.metadata.featureFlag);
|
|
433
441
|
if (flaggedResources.size === 0) return;
|
|
434
442
|
const adapter = this.get(FeatureFlagAdapterToken);
|
|
435
443
|
const flagResults = await this.batchEvaluateRefs(adapter, flaggedResources);
|
|
436
444
|
const filtered = resources.filter((item) => {
|
|
437
445
|
const ref = item.resource.metadata?.featureFlag;
|
|
438
|
-
if (!ref) return true;
|
|
446
|
+
if (!ref || !judged.has(item)) return true;
|
|
439
447
|
return this.isRefEnabled(ref, flagResults);
|
|
440
448
|
});
|
|
441
449
|
flowCtx.state.set("resources", filtered);
|
|
@@ -443,13 +451,14 @@ var FeatureFlagPlugin = class extends DynamicPlugin {
|
|
|
443
451
|
async filterListResourceTemplates(flowCtx) {
|
|
444
452
|
const { templates } = flowCtx.state;
|
|
445
453
|
if (!templates || templates.length === 0) return;
|
|
446
|
-
const
|
|
454
|
+
const judged = this.judgedItems(templates, (item) => ({ resource: item.template }));
|
|
455
|
+
const flaggedTemplates = this.collectFlagRefs(judged, (item) => item.template.metadata.featureFlag);
|
|
447
456
|
if (flaggedTemplates.size === 0) return;
|
|
448
457
|
const adapter = this.get(FeatureFlagAdapterToken);
|
|
449
458
|
const flagResults = await this.batchEvaluateRefs(adapter, flaggedTemplates);
|
|
450
459
|
const filtered = templates.filter((item) => {
|
|
451
460
|
const ref = item.template.metadata.featureFlag;
|
|
452
|
-
if (!ref) return true;
|
|
461
|
+
if (!ref || !judged.has(item)) return true;
|
|
453
462
|
return this.isRefEnabled(ref, flagResults);
|
|
454
463
|
});
|
|
455
464
|
flowCtx.state.set("templates", filtered);
|
|
@@ -457,13 +466,16 @@ var FeatureFlagPlugin = class extends DynamicPlugin {
|
|
|
457
466
|
async filterListPrompts(flowCtx) {
|
|
458
467
|
const { prompts } = flowCtx.state;
|
|
459
468
|
if (!prompts || prompts.length === 0) return;
|
|
460
|
-
const
|
|
469
|
+
const judged = this.judgedItems(prompts, (item) => ({
|
|
470
|
+
prompt: item.prompt
|
|
471
|
+
}));
|
|
472
|
+
const flaggedPrompts = this.collectFlagRefs(judged, (item) => item.prompt.metadata.featureFlag);
|
|
461
473
|
if (flaggedPrompts.size === 0) return;
|
|
462
474
|
const adapter = this.get(FeatureFlagAdapterToken);
|
|
463
475
|
const flagResults = await this.batchEvaluateRefs(adapter, flaggedPrompts);
|
|
464
476
|
const filtered = prompts.filter((item) => {
|
|
465
477
|
const ref = item.prompt.metadata?.featureFlag;
|
|
466
|
-
if (!ref) return true;
|
|
478
|
+
if (!ref || !judged.has(item)) return true;
|
|
467
479
|
return this.isRefEnabled(ref, flagResults);
|
|
468
480
|
});
|
|
469
481
|
flowCtx.state.set("prompts", filtered);
|
|
@@ -471,13 +483,14 @@ var FeatureFlagPlugin = class extends DynamicPlugin {
|
|
|
471
483
|
async filterSkills(flowCtx) {
|
|
472
484
|
const { skills } = flowCtx.state;
|
|
473
485
|
if (!skills || skills.length === 0) return;
|
|
474
|
-
const
|
|
486
|
+
const judged = this.judgedItems(skills, (skill) => ({ skill }));
|
|
487
|
+
const flaggedSkills = this.collectFlagRefs(judged, (skill) => skill.metadata.featureFlag);
|
|
475
488
|
if (flaggedSkills.size === 0) return;
|
|
476
489
|
const adapter = this.get(FeatureFlagAdapterToken);
|
|
477
490
|
const flagResults = await this.batchEvaluateRefs(adapter, flaggedSkills);
|
|
478
491
|
const filtered = skills.filter((skill) => {
|
|
479
492
|
const ref = skill.metadata.featureFlag;
|
|
480
|
-
if (!ref) return true;
|
|
493
|
+
if (!ref || !judged.has(skill)) return true;
|
|
481
494
|
return this.isRefEnabled(ref, flagResults);
|
|
482
495
|
});
|
|
483
496
|
flowCtx.state.set("skills", filtered);
|
|
@@ -524,6 +537,26 @@ var FeatureFlagPlugin = class extends DynamicPlugin {
|
|
|
524
537
|
// ─────────────────────────────────────────────────────────────────────────
|
|
525
538
|
// Private Helpers
|
|
526
539
|
// ─────────────────────────────────────────────────────────────────────────
|
|
540
|
+
/**
|
|
541
|
+
* The listed items this plugin instance judges: those whose gate (tools/call, resources/read,
|
|
542
|
+
* prompts/get, or `skills:filter` for skills) runs this instance's hook when the entry is served.
|
|
543
|
+
*
|
|
544
|
+
* List flows run every app's plugin over every app's entries, while a call is judged only by the
|
|
545
|
+
* plugin that covers the entry's app. With a FeatureFlagPlugin on each of two apps, filtering
|
|
546
|
+
* everything let one app's flags hide the other app's entries, which its own plugin still served
|
|
547
|
+
* by name. When the scope can't be read (a plugin used outside a server), every item is judged.
|
|
548
|
+
*/
|
|
549
|
+
judgedItems(items, entryOf) {
|
|
550
|
+
let scope;
|
|
551
|
+
try {
|
|
552
|
+
scope = this.get(ScopeEntry);
|
|
553
|
+
} catch {
|
|
554
|
+
scope = void 0;
|
|
555
|
+
}
|
|
556
|
+
if (!scope?.hooks) return new Set(items);
|
|
557
|
+
const judgingScope = scope;
|
|
558
|
+
return new Set(items.filter((item) => isEntryGatedBy(judgingScope, entryOf(item), this)));
|
|
559
|
+
}
|
|
527
560
|
/**
|
|
528
561
|
* Collect unique flag keys from items that have a featureFlag metadata.
|
|
529
562
|
*/
|
|
@@ -663,7 +696,7 @@ __decorateClass([
|
|
|
663
696
|
ListPromptsHook.Did("findPrompts", { priority: 50 })
|
|
664
697
|
], FeatureFlagPlugin.prototype, "filterListPrompts", 1);
|
|
665
698
|
__decorateClass([
|
|
666
|
-
FilterSkillsHook.Did("filterSkills", { priority: 50 })
|
|
699
|
+
FilterSkillsHook.Did("filterSkills", { priority: 50, appliesTo: "uncovered-apps" })
|
|
667
700
|
], FeatureFlagPlugin.prototype, "filterSkills", 1);
|
|
668
701
|
__decorateClass([
|
|
669
702
|
ToolHook.Will("execute", { priority: 50, appliesTo: "uncovered-apps" })
|
package/esm/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frontmcp/plugin-feature-flags",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.4",
|
|
4
4
|
"description": "Feature flag plugin for FrontMCP - dynamically gate MCP capabilities behind feature flags",
|
|
5
5
|
"author": "AgentFront <info@agentfront.dev>",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
}
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@frontmcp/sdk": "1.8.
|
|
52
|
+
"@frontmcp/sdk": "1.8.4"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
55
|
"@launchdarkly/node-server-sdk": "^9.0.0 || ^10.0.0",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"feature-flag.context.d.ts","sourceRoot":"","sources":["../src/feature-flag.context.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"feature-flag.context.d.ts","sourceRoot":"","sources":["../src/feature-flag.context.ts"],"names":[],"mappings":"AAAA,OAAO,EAA4C,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AAE/F,OAAO,KAAK,EAAE,kBAAkB,EAAE,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AAiCzF;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,eAAe,GAAG,SAAS,EAChC,MAAM,EAAE,IAAI,CAAC,wBAAwB,EAAE,gBAAgB,GAAG,oBAAoB,CAAC,GAC9E,kBAAkB,CAUpB"}
|
package/feature-flag.plugin.d.ts
CHANGED
|
@@ -89,6 +89,16 @@ export default class FeatureFlagPlugin extends DynamicPlugin<FeatureFlagPluginOp
|
|
|
89
89
|
* the other two did not, and three copies would drift apart the same way.
|
|
90
90
|
*/
|
|
91
91
|
private gateEntryExecution;
|
|
92
|
+
/**
|
|
93
|
+
* The listed items this plugin instance judges: those whose gate (tools/call, resources/read,
|
|
94
|
+
* prompts/get, or `skills:filter` for skills) runs this instance's hook when the entry is served.
|
|
95
|
+
*
|
|
96
|
+
* List flows run every app's plugin over every app's entries, while a call is judged only by the
|
|
97
|
+
* plugin that covers the entry's app. With a FeatureFlagPlugin on each of two apps, filtering
|
|
98
|
+
* everything let one app's flags hide the other app's entries, which its own plugin still served
|
|
99
|
+
* by name. When the scope can't be read (a plugin used outside a server), every item is judged.
|
|
100
|
+
*/
|
|
101
|
+
private judgedItems;
|
|
92
102
|
/**
|
|
93
103
|
* Collect unique flag keys from items that have a featureFlag metadata.
|
|
94
104
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"feature-flag.plugin.d.ts","sourceRoot":"","sources":["../src/feature-flag.plugin.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,
|
|
1
|
+
{"version":3,"file":"feature-flag.plugin.d.ts","sourceRoot":"","sources":["../src/feature-flag.plugin.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EAYb,KAAK,SAAS,EAGd,KAAK,YAAY,EAClB,MAAM,eAAe,CAAC;AAMvB,OAAO,KAAK,EAEV,wBAAwB,EACxB,6BAA6B,EAE9B,MAAM,sBAAsB,CAAC;AAa9B;;;;;;;;;;;;;;;;;;GAkBG;AAeH,MAAM,CAAC,OAAO,OAAO,iBAAkB,SAAQ,aAAa,CAAC,wBAAwB,EAAE,6BAA6B,CAAC;IACnH,OAAO,EAAE,wBAAwB,CAAC;gBAEtB,OAAO,EAAE,6BAA6B;IAKlD;;OAEG;IACH,OAAgB,gBAAgB,GAAI,SAAS,6BAA6B,KAAG,YAAY,EAAE,CA0FzF;IAMF;;;OAGG;IAEG,eAAe,CAAC,OAAO,EAAE,SAAS,CAAC,kBAAkB,CAAC;IAqB5D;;OAEG;IAEG,mBAAmB,CAAC,OAAO,EAAE,SAAS,CAAC,0BAA0B,CAAC;IAoBxE;;;;OAIG;IAEG,2BAA2B,CAAC,OAAO,EAAE,SAAS,CAAC,mCAAmC,CAAC;IAoBzF;;OAEG;IAEG,iBAAiB,CAAC,OAAO,EAAE,GAAG;IAsBpC;;;;;;;;OAQG;IAEG,YAAY,CAAC,OAAO,EAAE,SAAS,CAAC,eAAe,CAAC;IAsBtD;;;OAGG;IAEG,iBAAiB,CAAC,OAAO,EAAE,SAAS,CAAC,iBAAiB,CAAC;IAI7D;;;;;;OAMG;IAEG,gBAAgB,CAAC,OAAO,EAAE,GAAG;IAInC;;;;;OAKG;IAEG,aAAa,CAAC,OAAO,EAAE,GAAG;IAIhC;;;;;OAKG;IAEG,cAAc,CAAC,OAAO,EAAE,GAAG;IAMjC;;;;;OAKG;YACW,kBAAkB;IA+BhC;;;;;;;;OAQG;IACH,OAAO,CAAC,WAAW;IAYnB;;OAEG;IACH,OAAO,CAAC,eAAe;IAiBvB;;OAEG;YACW,iBAAiB;IAQ/B;;;;;;;OAOG;IACH,OAAO,CAAC,kBAAkB;IAW1B;;;;OAIG;IACH,OAAO,CAAC,YAAY;CAWrB"}
|
package/index.js
CHANGED
|
@@ -304,9 +304,13 @@ function verifiedSessionId(ctx) {
|
|
|
304
304
|
const verified = ctx.authInfo?.sessionId ?? ctx.authInfo?.extra?.["sessionId"];
|
|
305
305
|
return typeof verified === "string" && verified.length > 0 && verified !== import_sdk.STATELESS_SESSION_ID ? verified : void 0;
|
|
306
306
|
}
|
|
307
|
+
function signedInUserId(ctx) {
|
|
308
|
+
const userId = ctx.authInfo?.extra?.["sub"] ?? ctx.authInfo?.extra?.["userId"] ?? ctx.authInfo?.clientId;
|
|
309
|
+
return (0, import_sdk.isAnonymousSubject)(userId) ? void 0 : userId;
|
|
310
|
+
}
|
|
307
311
|
function buildFeatureFlagContext(ctx, config) {
|
|
308
312
|
if (!ctx) return {};
|
|
309
|
-
const userId = config.userIdResolver ? config.userIdResolver(ctx) : ctx
|
|
313
|
+
const userId = config.userIdResolver ? config.userIdResolver(ctx) : signedInUserId(ctx);
|
|
310
314
|
return {
|
|
311
315
|
userId: userId ?? void 0,
|
|
312
316
|
sessionId: verifiedSessionId(ctx),
|
|
@@ -417,13 +421,14 @@ var FeatureFlagPlugin = class extends import_sdk3.DynamicPlugin {
|
|
|
417
421
|
async filterListTools(flowCtx) {
|
|
418
422
|
const { tools } = flowCtx.state;
|
|
419
423
|
if (!tools || tools.length === 0) return;
|
|
420
|
-
const
|
|
424
|
+
const judged = this.judgedItems(tools, (item) => ({ tool: item.tool }));
|
|
425
|
+
const flaggedTools = this.collectFlagRefs(judged, (item) => item.tool.metadata.featureFlag);
|
|
421
426
|
if (flaggedTools.size === 0) return;
|
|
422
427
|
const adapter = this.get(FeatureFlagAdapterToken);
|
|
423
428
|
const flagResults = await this.batchEvaluateRefs(adapter, flaggedTools);
|
|
424
429
|
const filtered = tools.filter((item) => {
|
|
425
430
|
const ref = item.tool.metadata?.featureFlag;
|
|
426
|
-
if (!ref) return true;
|
|
431
|
+
if (!ref || !judged.has(item)) return true;
|
|
427
432
|
return this.isRefEnabled(ref, flagResults);
|
|
428
433
|
});
|
|
429
434
|
flowCtx.state.set("tools", filtered);
|
|
@@ -431,13 +436,14 @@ var FeatureFlagPlugin = class extends import_sdk3.DynamicPlugin {
|
|
|
431
436
|
async filterListResources(flowCtx) {
|
|
432
437
|
const { resources } = flowCtx.state;
|
|
433
438
|
if (!resources || resources.length === 0) return;
|
|
434
|
-
const
|
|
439
|
+
const judged = this.judgedItems(resources, (item) => ({ resource: item.resource }));
|
|
440
|
+
const flaggedResources = this.collectFlagRefs(judged, (item) => item.resource.metadata.featureFlag);
|
|
435
441
|
if (flaggedResources.size === 0) return;
|
|
436
442
|
const adapter = this.get(FeatureFlagAdapterToken);
|
|
437
443
|
const flagResults = await this.batchEvaluateRefs(adapter, flaggedResources);
|
|
438
444
|
const filtered = resources.filter((item) => {
|
|
439
445
|
const ref = item.resource.metadata?.featureFlag;
|
|
440
|
-
if (!ref) return true;
|
|
446
|
+
if (!ref || !judged.has(item)) return true;
|
|
441
447
|
return this.isRefEnabled(ref, flagResults);
|
|
442
448
|
});
|
|
443
449
|
flowCtx.state.set("resources", filtered);
|
|
@@ -445,13 +451,14 @@ var FeatureFlagPlugin = class extends import_sdk3.DynamicPlugin {
|
|
|
445
451
|
async filterListResourceTemplates(flowCtx) {
|
|
446
452
|
const { templates } = flowCtx.state;
|
|
447
453
|
if (!templates || templates.length === 0) return;
|
|
448
|
-
const
|
|
454
|
+
const judged = this.judgedItems(templates, (item) => ({ resource: item.template }));
|
|
455
|
+
const flaggedTemplates = this.collectFlagRefs(judged, (item) => item.template.metadata.featureFlag);
|
|
449
456
|
if (flaggedTemplates.size === 0) return;
|
|
450
457
|
const adapter = this.get(FeatureFlagAdapterToken);
|
|
451
458
|
const flagResults = await this.batchEvaluateRefs(adapter, flaggedTemplates);
|
|
452
459
|
const filtered = templates.filter((item) => {
|
|
453
460
|
const ref = item.template.metadata.featureFlag;
|
|
454
|
-
if (!ref) return true;
|
|
461
|
+
if (!ref || !judged.has(item)) return true;
|
|
455
462
|
return this.isRefEnabled(ref, flagResults);
|
|
456
463
|
});
|
|
457
464
|
flowCtx.state.set("templates", filtered);
|
|
@@ -459,13 +466,16 @@ var FeatureFlagPlugin = class extends import_sdk3.DynamicPlugin {
|
|
|
459
466
|
async filterListPrompts(flowCtx) {
|
|
460
467
|
const { prompts } = flowCtx.state;
|
|
461
468
|
if (!prompts || prompts.length === 0) return;
|
|
462
|
-
const
|
|
469
|
+
const judged = this.judgedItems(prompts, (item) => ({
|
|
470
|
+
prompt: item.prompt
|
|
471
|
+
}));
|
|
472
|
+
const flaggedPrompts = this.collectFlagRefs(judged, (item) => item.prompt.metadata.featureFlag);
|
|
463
473
|
if (flaggedPrompts.size === 0) return;
|
|
464
474
|
const adapter = this.get(FeatureFlagAdapterToken);
|
|
465
475
|
const flagResults = await this.batchEvaluateRefs(adapter, flaggedPrompts);
|
|
466
476
|
const filtered = prompts.filter((item) => {
|
|
467
477
|
const ref = item.prompt.metadata?.featureFlag;
|
|
468
|
-
if (!ref) return true;
|
|
478
|
+
if (!ref || !judged.has(item)) return true;
|
|
469
479
|
return this.isRefEnabled(ref, flagResults);
|
|
470
480
|
});
|
|
471
481
|
flowCtx.state.set("prompts", filtered);
|
|
@@ -473,13 +483,14 @@ var FeatureFlagPlugin = class extends import_sdk3.DynamicPlugin {
|
|
|
473
483
|
async filterSkills(flowCtx) {
|
|
474
484
|
const { skills } = flowCtx.state;
|
|
475
485
|
if (!skills || skills.length === 0) return;
|
|
476
|
-
const
|
|
486
|
+
const judged = this.judgedItems(skills, (skill) => ({ skill }));
|
|
487
|
+
const flaggedSkills = this.collectFlagRefs(judged, (skill) => skill.metadata.featureFlag);
|
|
477
488
|
if (flaggedSkills.size === 0) return;
|
|
478
489
|
const adapter = this.get(FeatureFlagAdapterToken);
|
|
479
490
|
const flagResults = await this.batchEvaluateRefs(adapter, flaggedSkills);
|
|
480
491
|
const filtered = skills.filter((skill) => {
|
|
481
492
|
const ref = skill.metadata.featureFlag;
|
|
482
|
-
if (!ref) return true;
|
|
493
|
+
if (!ref || !judged.has(skill)) return true;
|
|
483
494
|
return this.isRefEnabled(ref, flagResults);
|
|
484
495
|
});
|
|
485
496
|
flowCtx.state.set("skills", filtered);
|
|
@@ -526,6 +537,26 @@ var FeatureFlagPlugin = class extends import_sdk3.DynamicPlugin {
|
|
|
526
537
|
// ─────────────────────────────────────────────────────────────────────────
|
|
527
538
|
// Private Helpers
|
|
528
539
|
// ─────────────────────────────────────────────────────────────────────────
|
|
540
|
+
/**
|
|
541
|
+
* The listed items this plugin instance judges: those whose gate (tools/call, resources/read,
|
|
542
|
+
* prompts/get, or `skills:filter` for skills) runs this instance's hook when the entry is served.
|
|
543
|
+
*
|
|
544
|
+
* List flows run every app's plugin over every app's entries, while a call is judged only by the
|
|
545
|
+
* plugin that covers the entry's app. With a FeatureFlagPlugin on each of two apps, filtering
|
|
546
|
+
* everything let one app's flags hide the other app's entries, which its own plugin still served
|
|
547
|
+
* by name. When the scope can't be read (a plugin used outside a server), every item is judged.
|
|
548
|
+
*/
|
|
549
|
+
judgedItems(items, entryOf) {
|
|
550
|
+
let scope;
|
|
551
|
+
try {
|
|
552
|
+
scope = this.get(import_sdk3.ScopeEntry);
|
|
553
|
+
} catch {
|
|
554
|
+
scope = void 0;
|
|
555
|
+
}
|
|
556
|
+
if (!scope?.hooks) return new Set(items);
|
|
557
|
+
const judgingScope = scope;
|
|
558
|
+
return new Set(items.filter((item) => (0, import_sdk3.isEntryGatedBy)(judgingScope, entryOf(item), this)));
|
|
559
|
+
}
|
|
529
560
|
/**
|
|
530
561
|
* Collect unique flag keys from items that have a featureFlag metadata.
|
|
531
562
|
*/
|
|
@@ -665,7 +696,7 @@ __decorateClass([
|
|
|
665
696
|
ListPromptsHook.Did("findPrompts", { priority: 50 })
|
|
666
697
|
], FeatureFlagPlugin.prototype, "filterListPrompts", 1);
|
|
667
698
|
__decorateClass([
|
|
668
|
-
FilterSkillsHook.Did("filterSkills", { priority: 50 })
|
|
699
|
+
FilterSkillsHook.Did("filterSkills", { priority: 50, appliesTo: "uncovered-apps" })
|
|
669
700
|
], FeatureFlagPlugin.prototype, "filterSkills", 1);
|
|
670
701
|
__decorateClass([
|
|
671
702
|
import_sdk3.ToolHook.Will("execute", { priority: 50, appliesTo: "uncovered-apps" })
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frontmcp/plugin-feature-flags",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.4",
|
|
4
4
|
"description": "Feature flag plugin for FrontMCP - dynamically gate MCP capabilities behind feature flags",
|
|
5
5
|
"author": "AgentFront <info@agentfront.dev>",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
}
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@frontmcp/sdk": "1.8.
|
|
52
|
+
"@frontmcp/sdk": "1.8.4"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
55
|
"@launchdarkly/node-server-sdk": "^9.0.0 || ^10.0.0",
|