@frontmcp/plugin-feature-flags 1.8.3 → 1.8.5

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 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.authInfo?.extra?.["sub"] ?? ctx.authInfo?.extra?.["userId"] ?? ctx.authInfo?.clientId;
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 flaggedTools = this.collectFlagRefs(tools, (item) => item.tool.metadata?.featureFlag);
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 flaggedResources = this.collectFlagRefs(resources, (item) => item.resource.metadata?.featureFlag);
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 flaggedTemplates = this.collectFlagRefs(templates, (item) => item.template.metadata.featureFlag);
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 flaggedPrompts = this.collectFlagRefs(prompts, (item) => item.prompt.metadata?.featureFlag);
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 flaggedSkills = this.collectFlagRefs(skills, (skill) => skill.metadata.featureFlag);
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",
3
+ "version": "1.8.5",
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.3"
52
+ "@frontmcp/sdk": "1.8.5"
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,EAAwB,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AAE3E,OAAO,KAAK,EAAE,kBAAkB,EAAE,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AAkBzF;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,eAAe,GAAG,SAAS,EAChC,MAAM,EAAE,IAAI,CAAC,wBAAwB,EAAE,gBAAgB,GAAG,oBAAoB,CAAC,GAC9E,kBAAkB,CAcpB"}
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"}
@@ -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,EAUb,KAAK,SAAS,EACd,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;IAmB5D;;OAEG;IAEG,mBAAmB,CAAC,OAAO,EAAE,SAAS,CAAC,0BAA0B,CAAC;IAmBxE;;;;OAIG;IAEG,2BAA2B,CAAC,OAAO,EAAE,SAAS,CAAC,mCAAmC,CAAC;IAmBzF;;OAEG;IAEG,iBAAiB,CAAC,OAAO,EAAE,GAAG;IAmBpC;;;;;;;;OAQG;IAEG,YAAY,CAAC,OAAO,EAAE,SAAS,CAAC,eAAe,CAAC;IAmBtD;;;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;;OAEG;IACH,OAAO,CAAC,eAAe;IAcvB;;OAEG;YACW,iBAAiB;IAQ/B;;;;;;;OAOG;IACH,OAAO,CAAC,kBAAkB;IAW1B;;;;OAIG;IACH,OAAO,CAAC,YAAY;CAWrB"}
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.authInfo?.extra?.["sub"] ?? ctx.authInfo?.extra?.["userId"] ?? ctx.authInfo?.clientId;
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 flaggedTools = this.collectFlagRefs(tools, (item) => item.tool.metadata?.featureFlag);
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 flaggedResources = this.collectFlagRefs(resources, (item) => item.resource.metadata?.featureFlag);
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 flaggedTemplates = this.collectFlagRefs(templates, (item) => item.template.metadata.featureFlag);
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 flaggedPrompts = this.collectFlagRefs(prompts, (item) => item.prompt.metadata?.featureFlag);
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 flaggedSkills = this.collectFlagRefs(skills, (skill) => skill.metadata.featureFlag);
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",
3
+ "version": "1.8.5",
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.3"
52
+ "@frontmcp/sdk": "1.8.5"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "@launchdarkly/node-server-sdk": "^9.0.0 || ^10.0.0",