@frontmcp/plugin-feature-flags 1.8.2 → 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 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,12 +299,21 @@ var StaticFeatureFlagAdapter = class {
297
299
  };
298
300
 
299
301
  // plugins/plugin-feature-flags/src/feature-flag.context.ts
302
+ import { isAnonymousSubject, STATELESS_SESSION_ID } from "@frontmcp/sdk";
303
+ function verifiedSessionId(ctx) {
304
+ const verified = ctx.authInfo?.sessionId ?? ctx.authInfo?.extra?.["sessionId"];
305
+ return typeof verified === "string" && verified.length > 0 && verified !== STATELESS_SESSION_ID ? verified : void 0;
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
+ }
300
311
  function buildFeatureFlagContext(ctx, config) {
301
312
  if (!ctx) return {};
302
- 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);
303
314
  return {
304
315
  userId: userId ?? void 0,
305
- sessionId: ctx.sessionId,
316
+ sessionId: verifiedSessionId(ctx),
306
317
  attributes: config.attributesResolver ? config.attributesResolver(ctx) : {}
307
318
  };
308
319
  }
@@ -410,13 +421,14 @@ var FeatureFlagPlugin = class extends DynamicPlugin {
410
421
  async filterListTools(flowCtx) {
411
422
  const { tools } = flowCtx.state;
412
423
  if (!tools || tools.length === 0) return;
413
- 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);
414
426
  if (flaggedTools.size === 0) return;
415
427
  const adapter = this.get(FeatureFlagAdapterToken);
416
428
  const flagResults = await this.batchEvaluateRefs(adapter, flaggedTools);
417
429
  const filtered = tools.filter((item) => {
418
430
  const ref = item.tool.metadata?.featureFlag;
419
- if (!ref) return true;
431
+ if (!ref || !judged.has(item)) return true;
420
432
  return this.isRefEnabled(ref, flagResults);
421
433
  });
422
434
  flowCtx.state.set("tools", filtered);
@@ -424,13 +436,14 @@ var FeatureFlagPlugin = class extends DynamicPlugin {
424
436
  async filterListResources(flowCtx) {
425
437
  const { resources } = flowCtx.state;
426
438
  if (!resources || resources.length === 0) return;
427
- 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);
428
441
  if (flaggedResources.size === 0) return;
429
442
  const adapter = this.get(FeatureFlagAdapterToken);
430
443
  const flagResults = await this.batchEvaluateRefs(adapter, flaggedResources);
431
444
  const filtered = resources.filter((item) => {
432
445
  const ref = item.resource.metadata?.featureFlag;
433
- if (!ref) return true;
446
+ if (!ref || !judged.has(item)) return true;
434
447
  return this.isRefEnabled(ref, flagResults);
435
448
  });
436
449
  flowCtx.state.set("resources", filtered);
@@ -438,13 +451,14 @@ var FeatureFlagPlugin = class extends DynamicPlugin {
438
451
  async filterListResourceTemplates(flowCtx) {
439
452
  const { templates } = flowCtx.state;
440
453
  if (!templates || templates.length === 0) return;
441
- 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);
442
456
  if (flaggedTemplates.size === 0) return;
443
457
  const adapter = this.get(FeatureFlagAdapterToken);
444
458
  const flagResults = await this.batchEvaluateRefs(adapter, flaggedTemplates);
445
459
  const filtered = templates.filter((item) => {
446
460
  const ref = item.template.metadata.featureFlag;
447
- if (!ref) return true;
461
+ if (!ref || !judged.has(item)) return true;
448
462
  return this.isRefEnabled(ref, flagResults);
449
463
  });
450
464
  flowCtx.state.set("templates", filtered);
@@ -452,13 +466,16 @@ var FeatureFlagPlugin = class extends DynamicPlugin {
452
466
  async filterListPrompts(flowCtx) {
453
467
  const { prompts } = flowCtx.state;
454
468
  if (!prompts || prompts.length === 0) return;
455
- 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);
456
473
  if (flaggedPrompts.size === 0) return;
457
474
  const adapter = this.get(FeatureFlagAdapterToken);
458
475
  const flagResults = await this.batchEvaluateRefs(adapter, flaggedPrompts);
459
476
  const filtered = prompts.filter((item) => {
460
477
  const ref = item.prompt.metadata?.featureFlag;
461
- if (!ref) return true;
478
+ if (!ref || !judged.has(item)) return true;
462
479
  return this.isRefEnabled(ref, flagResults);
463
480
  });
464
481
  flowCtx.state.set("prompts", filtered);
@@ -466,13 +483,14 @@ var FeatureFlagPlugin = class extends DynamicPlugin {
466
483
  async filterSkills(flowCtx) {
467
484
  const { skills } = flowCtx.state;
468
485
  if (!skills || skills.length === 0) return;
469
- 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);
470
488
  if (flaggedSkills.size === 0) return;
471
489
  const adapter = this.get(FeatureFlagAdapterToken);
472
490
  const flagResults = await this.batchEvaluateRefs(adapter, flaggedSkills);
473
491
  const filtered = skills.filter((skill) => {
474
492
  const ref = skill.metadata.featureFlag;
475
- if (!ref) return true;
493
+ if (!ref || !judged.has(skill)) return true;
476
494
  return this.isRefEnabled(ref, flagResults);
477
495
  });
478
496
  flowCtx.state.set("skills", filtered);
@@ -519,6 +537,26 @@ var FeatureFlagPlugin = class extends DynamicPlugin {
519
537
  // ─────────────────────────────────────────────────────────────────────────
520
538
  // Private Helpers
521
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
+ }
522
560
  /**
523
561
  * Collect unique flag keys from items that have a featureFlag metadata.
524
562
  */
@@ -658,24 +696,26 @@ __decorateClass([
658
696
  ListPromptsHook.Did("findPrompts", { priority: 50 })
659
697
  ], FeatureFlagPlugin.prototype, "filterListPrompts", 1);
660
698
  __decorateClass([
661
- FilterSkillsHook.Did("filterSkills", { priority: 50 })
699
+ FilterSkillsHook.Did("filterSkills", { priority: 50, appliesTo: "uncovered-apps" })
662
700
  ], FeatureFlagPlugin.prototype, "filterSkills", 1);
663
701
  __decorateClass([
664
- ToolHook.Will("execute", { priority: 50 })
702
+ ToolHook.Will("execute", { priority: 50, appliesTo: "uncovered-apps" })
665
703
  ], FeatureFlagPlugin.prototype, "gateToolExecution", 1);
666
704
  __decorateClass([
667
- ReadResourceHook.Will("execute", { priority: 50 })
705
+ ReadResourceHook.Will("execute", { priority: 50, appliesTo: "uncovered-apps" })
668
706
  ], FeatureFlagPlugin.prototype, "gateResourceRead", 1);
669
707
  __decorateClass([
670
- GetPromptHook.Will("execute", { priority: 50 })
708
+ GetPromptHook.Will("execute", { priority: 50, appliesTo: "uncovered-apps" })
671
709
  ], FeatureFlagPlugin.prototype, "gatePromptGet", 1);
672
710
  __decorateClass([
673
- CompleteHook.Will("complete", { priority: 50 })
711
+ CompleteHook.Will("complete", { priority: 50, appliesTo: "uncovered-apps" })
674
712
  ], FeatureFlagPlugin.prototype, "gateCompletion", 1);
675
713
  FeatureFlagPlugin = __decorateClass([
676
714
  Plugin({
677
715
  name: "feature-flags",
678
716
  description: "Feature flag-based capability filtering for MCP",
717
+ // A server where an entry declares `featureFlag` and none of these gates reaches it refuses to start.
718
+ enforcesMetadata: ["featureFlag"],
679
719
  providers: [],
680
720
  contextExtensions: [
681
721
  {
package/esm/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frontmcp/plugin-feature-flags",
3
- "version": "1.8.2",
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.2"
52
+ "@frontmcp/sdk": "1.8.4"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "@launchdarkly/node-server-sdk": "^9.0.0 || ^10.0.0",
@@ -1,4 +1,4 @@
1
- import type { FrontMcpContext } from '@frontmcp/sdk';
1
+ import { type FrontMcpContext } from '@frontmcp/sdk';
2
2
  import type { FeatureFlagContext, FeatureFlagPluginOptions } from './feature-flag.types';
3
3
  /**
4
4
  * Build the adapter evaluation context for the current caller.
@@ -1 +1 @@
1
- {"version":3,"file":"feature-flag.context.d.ts","sourceRoot":"","sources":["../src/feature-flag.context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAErD,OAAO,KAAK,EAAE,kBAAkB,EAAE,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AAEzF;;;;;;;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;AAaH,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
@@ -255,7 +255,7 @@ __export(index_exports, {
255
255
  module.exports = __toCommonJS(index_exports);
256
256
 
257
257
  // plugins/plugin-feature-flags/src/feature-flag.plugin.ts
258
- var import_sdk2 = require("@frontmcp/sdk");
258
+ var import_sdk3 = require("@frontmcp/sdk");
259
259
 
260
260
  // plugins/plugin-feature-flags/src/adapters/static.adapter.ts
261
261
  var StaticFeatureFlagAdapter = class {
@@ -299,12 +299,21 @@ var StaticFeatureFlagAdapter = class {
299
299
  };
300
300
 
301
301
  // plugins/plugin-feature-flags/src/feature-flag.context.ts
302
+ var import_sdk = require("@frontmcp/sdk");
303
+ function verifiedSessionId(ctx) {
304
+ const verified = ctx.authInfo?.sessionId ?? ctx.authInfo?.extra?.["sessionId"];
305
+ return typeof verified === "string" && verified.length > 0 && verified !== import_sdk.STATELESS_SESSION_ID ? verified : void 0;
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
+ }
302
311
  function buildFeatureFlagContext(ctx, config) {
303
312
  if (!ctx) return {};
304
- 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);
305
314
  return {
306
315
  userId: userId ?? void 0,
307
- sessionId: ctx.sessionId,
316
+ sessionId: verifiedSessionId(ctx),
308
317
  attributes: config.attributesResolver ? config.attributesResolver(ctx) : {}
309
318
  };
310
319
  }
@@ -321,7 +330,7 @@ var FeatureFlagAccessorToken = /* @__PURE__ */ Symbol(
321
330
  );
322
331
 
323
332
  // plugins/plugin-feature-flags/src/providers/feature-flag-accessor.provider.ts
324
- var import_sdk = require("@frontmcp/sdk");
333
+ var import_sdk2 = require("@frontmcp/sdk");
325
334
  var FeatureFlagAccessor = class {
326
335
  adapter;
327
336
  ctx;
@@ -387,10 +396,10 @@ var FeatureFlagAccessor = class {
387
396
  }
388
397
  };
389
398
  FeatureFlagAccessor = __decorateClass([
390
- (0, import_sdk.Provider)({
399
+ (0, import_sdk2.Provider)({
391
400
  name: "provider:feature-flags:accessor",
392
401
  description: "Context-scoped accessor for feature flag evaluation",
393
- scope: import_sdk.ProviderScope.CONTEXT
402
+ scope: import_sdk2.ProviderScope.CONTEXT
394
403
  })
395
404
  ], FeatureFlagAccessor);
396
405
  function createFeatureFlagAccessor(adapter, ctx, config) {
@@ -398,12 +407,12 @@ function createFeatureFlagAccessor(adapter, ctx, config) {
398
407
  }
399
408
 
400
409
  // plugins/plugin-feature-flags/src/feature-flag.plugin.ts
401
- var ListPromptsHook = (0, import_sdk2.FlowHooksOf)("prompts:list-prompts");
402
- var ReadResourceHook = (0, import_sdk2.FlowHooksOf)("resources:read-resource");
403
- var GetPromptHook = (0, import_sdk2.FlowHooksOf)("prompts:get-prompt");
404
- var CompleteHook = (0, import_sdk2.FlowHooksOf)("completion:complete");
405
- var FilterSkillsHook = (0, import_sdk2.FlowHooksOf)("skills:filter");
406
- var FeatureFlagPlugin = class extends import_sdk2.DynamicPlugin {
410
+ var ListPromptsHook = (0, import_sdk3.FlowHooksOf)("prompts:list-prompts");
411
+ var ReadResourceHook = (0, import_sdk3.FlowHooksOf)("resources:read-resource");
412
+ var GetPromptHook = (0, import_sdk3.FlowHooksOf)("prompts:get-prompt");
413
+ var CompleteHook = (0, import_sdk3.FlowHooksOf)("completion:complete");
414
+ var FilterSkillsHook = (0, import_sdk3.FlowHooksOf)("skills:filter");
415
+ var FeatureFlagPlugin = class extends import_sdk3.DynamicPlugin {
407
416
  options;
408
417
  constructor(options) {
409
418
  super();
@@ -412,13 +421,14 @@ var FeatureFlagPlugin = class extends import_sdk2.DynamicPlugin {
412
421
  async filterListTools(flowCtx) {
413
422
  const { tools } = flowCtx.state;
414
423
  if (!tools || tools.length === 0) return;
415
- 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);
416
426
  if (flaggedTools.size === 0) return;
417
427
  const adapter = this.get(FeatureFlagAdapterToken);
418
428
  const flagResults = await this.batchEvaluateRefs(adapter, flaggedTools);
419
429
  const filtered = tools.filter((item) => {
420
430
  const ref = item.tool.metadata?.featureFlag;
421
- if (!ref) return true;
431
+ if (!ref || !judged.has(item)) return true;
422
432
  return this.isRefEnabled(ref, flagResults);
423
433
  });
424
434
  flowCtx.state.set("tools", filtered);
@@ -426,13 +436,14 @@ var FeatureFlagPlugin = class extends import_sdk2.DynamicPlugin {
426
436
  async filterListResources(flowCtx) {
427
437
  const { resources } = flowCtx.state;
428
438
  if (!resources || resources.length === 0) return;
429
- 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);
430
441
  if (flaggedResources.size === 0) return;
431
442
  const adapter = this.get(FeatureFlagAdapterToken);
432
443
  const flagResults = await this.batchEvaluateRefs(adapter, flaggedResources);
433
444
  const filtered = resources.filter((item) => {
434
445
  const ref = item.resource.metadata?.featureFlag;
435
- if (!ref) return true;
446
+ if (!ref || !judged.has(item)) return true;
436
447
  return this.isRefEnabled(ref, flagResults);
437
448
  });
438
449
  flowCtx.state.set("resources", filtered);
@@ -440,13 +451,14 @@ var FeatureFlagPlugin = class extends import_sdk2.DynamicPlugin {
440
451
  async filterListResourceTemplates(flowCtx) {
441
452
  const { templates } = flowCtx.state;
442
453
  if (!templates || templates.length === 0) return;
443
- 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);
444
456
  if (flaggedTemplates.size === 0) return;
445
457
  const adapter = this.get(FeatureFlagAdapterToken);
446
458
  const flagResults = await this.batchEvaluateRefs(adapter, flaggedTemplates);
447
459
  const filtered = templates.filter((item) => {
448
460
  const ref = item.template.metadata.featureFlag;
449
- if (!ref) return true;
461
+ if (!ref || !judged.has(item)) return true;
450
462
  return this.isRefEnabled(ref, flagResults);
451
463
  });
452
464
  flowCtx.state.set("templates", filtered);
@@ -454,13 +466,16 @@ var FeatureFlagPlugin = class extends import_sdk2.DynamicPlugin {
454
466
  async filterListPrompts(flowCtx) {
455
467
  const { prompts } = flowCtx.state;
456
468
  if (!prompts || prompts.length === 0) return;
457
- 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);
458
473
  if (flaggedPrompts.size === 0) return;
459
474
  const adapter = this.get(FeatureFlagAdapterToken);
460
475
  const flagResults = await this.batchEvaluateRefs(adapter, flaggedPrompts);
461
476
  const filtered = prompts.filter((item) => {
462
477
  const ref = item.prompt.metadata?.featureFlag;
463
- if (!ref) return true;
478
+ if (!ref || !judged.has(item)) return true;
464
479
  return this.isRefEnabled(ref, flagResults);
465
480
  });
466
481
  flowCtx.state.set("prompts", filtered);
@@ -468,13 +483,14 @@ var FeatureFlagPlugin = class extends import_sdk2.DynamicPlugin {
468
483
  async filterSkills(flowCtx) {
469
484
  const { skills } = flowCtx.state;
470
485
  if (!skills || skills.length === 0) return;
471
- 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);
472
488
  if (flaggedSkills.size === 0) return;
473
489
  const adapter = this.get(FeatureFlagAdapterToken);
474
490
  const flagResults = await this.batchEvaluateRefs(adapter, flaggedSkills);
475
491
  const filtered = skills.filter((skill) => {
476
492
  const ref = skill.metadata.featureFlag;
477
- if (!ref) return true;
493
+ if (!ref || !judged.has(skill)) return true;
478
494
  return this.isRefEnabled(ref, flagResults);
479
495
  });
480
496
  flowCtx.state.set("skills", filtered);
@@ -521,6 +537,26 @@ var FeatureFlagPlugin = class extends import_sdk2.DynamicPlugin {
521
537
  // ─────────────────────────────────────────────────────────────────────────
522
538
  // Private Helpers
523
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
+ }
524
560
  /**
525
561
  * Collect unique flag keys from items that have a featureFlag metadata.
526
562
  */
@@ -554,7 +590,7 @@ var FeatureFlagPlugin = class extends import_sdk2.DynamicPlugin {
554
590
  */
555
591
  currentFlagContext() {
556
592
  try {
557
- const ctx = this.get(import_sdk2.FrontMcpContextStorage)?.getStore();
593
+ const ctx = this.get(import_sdk3.FrontMcpContextStorage)?.getStore();
558
594
  if (!ctx) return {};
559
595
  return buildFeatureFlagContext(ctx, this.options);
560
596
  } catch {
@@ -641,43 +677,45 @@ __publicField(FeatureFlagPlugin, "dynamicProviders", (options) => {
641
677
  providers.push({
642
678
  name: "feature-flags:accessor",
643
679
  provide: FeatureFlagAccessorToken,
644
- scope: import_sdk2.ProviderScope.CONTEXT,
645
- inject: () => [FeatureFlagAdapterToken, import_sdk2.FRONTMCP_CONTEXT, FeatureFlagConfigToken],
680
+ scope: import_sdk3.ProviderScope.CONTEXT,
681
+ inject: () => [FeatureFlagAdapterToken, import_sdk3.FRONTMCP_CONTEXT, FeatureFlagConfigToken],
646
682
  useFactory: (adapter, ctx, cfg) => createFeatureFlagAccessor(adapter, ctx, cfg)
647
683
  });
648
684
  return providers;
649
685
  });
650
686
  __decorateClass([
651
- import_sdk2.ListToolsHook.Did("findTools", { priority: 50 })
687
+ import_sdk3.ListToolsHook.Did("findTools", { priority: 50 })
652
688
  ], FeatureFlagPlugin.prototype, "filterListTools", 1);
653
689
  __decorateClass([
654
- import_sdk2.ListResourcesHook.Did("findResources", { priority: 50 })
690
+ import_sdk3.ListResourcesHook.Did("findResources", { priority: 50 })
655
691
  ], FeatureFlagPlugin.prototype, "filterListResources", 1);
656
692
  __decorateClass([
657
- import_sdk2.ListResourceTemplatesHook.Did("findTemplates", { priority: 50 })
693
+ import_sdk3.ListResourceTemplatesHook.Did("findTemplates", { priority: 50 })
658
694
  ], FeatureFlagPlugin.prototype, "filterListResourceTemplates", 1);
659
695
  __decorateClass([
660
696
  ListPromptsHook.Did("findPrompts", { priority: 50 })
661
697
  ], FeatureFlagPlugin.prototype, "filterListPrompts", 1);
662
698
  __decorateClass([
663
- FilterSkillsHook.Did("filterSkills", { priority: 50 })
699
+ FilterSkillsHook.Did("filterSkills", { priority: 50, appliesTo: "uncovered-apps" })
664
700
  ], FeatureFlagPlugin.prototype, "filterSkills", 1);
665
701
  __decorateClass([
666
- import_sdk2.ToolHook.Will("execute", { priority: 50 })
702
+ import_sdk3.ToolHook.Will("execute", { priority: 50, appliesTo: "uncovered-apps" })
667
703
  ], FeatureFlagPlugin.prototype, "gateToolExecution", 1);
668
704
  __decorateClass([
669
- ReadResourceHook.Will("execute", { priority: 50 })
705
+ ReadResourceHook.Will("execute", { priority: 50, appliesTo: "uncovered-apps" })
670
706
  ], FeatureFlagPlugin.prototype, "gateResourceRead", 1);
671
707
  __decorateClass([
672
- GetPromptHook.Will("execute", { priority: 50 })
708
+ GetPromptHook.Will("execute", { priority: 50, appliesTo: "uncovered-apps" })
673
709
  ], FeatureFlagPlugin.prototype, "gatePromptGet", 1);
674
710
  __decorateClass([
675
- CompleteHook.Will("complete", { priority: 50 })
711
+ CompleteHook.Will("complete", { priority: 50, appliesTo: "uncovered-apps" })
676
712
  ], FeatureFlagPlugin.prototype, "gateCompletion", 1);
677
713
  FeatureFlagPlugin = __decorateClass([
678
- (0, import_sdk2.Plugin)({
714
+ (0, import_sdk3.Plugin)({
679
715
  name: "feature-flags",
680
716
  description: "Feature flag-based capability filtering for MCP",
717
+ // A server where an entry declares `featureFlag` and none of these gates reaches it refuses to start.
718
+ enforcesMetadata: ["featureFlag"],
681
719
  providers: [],
682
720
  contextExtensions: [
683
721
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frontmcp/plugin-feature-flags",
3
- "version": "1.8.2",
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.2"
52
+ "@frontmcp/sdk": "1.8.4"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "@launchdarkly/node-server-sdk": "^9.0.0 || ^10.0.0",