@frontmcp/plugin-feature-flags 1.7.2 → 1.8.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.
@@ -10,7 +10,17 @@ export interface FeatureFlagAdapter {
10
10
  isEnabled(flagKey: string, context: FeatureFlagContext): Promise<boolean>;
11
11
  /** Get the variant for a flag (for multi-variate flags). */
12
12
  getVariant(flagKey: string, context: FeatureFlagContext): Promise<FeatureFlagVariant>;
13
- /** Batch evaluate multiple flags at once. */
13
+ /**
14
+ * Batch evaluate multiple flags at once.
15
+ *
16
+ * An adapter MAY omit a key it has no opinion on — a flag it has never heard of. An absent
17
+ * key means "unknown", and the caller applies the ref's `defaultValue`; a key present with
18
+ * `false` means the flag is genuinely disabled and `defaultValue` does NOT apply. Conflating
19
+ * the two makes `defaultValue` dead code and makes listing disagree with the execution gate.
20
+ *
21
+ * Adapters backed by a remote service (Split.io, LaunchDarkly, Unleash) always have an
22
+ * opinion, because the service applies its own default — they answer for every key.
23
+ */
14
24
  evaluateFlags(flagKeys: string[], context: FeatureFlagContext): Promise<Map<string, boolean>>;
15
25
  /** Destroy the adapter (disconnect, cleanup). */
16
26
  destroy(): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"feature-flag-adapter.interface.d.ts","sourceRoot":"","sources":["../../src/adapters/feature-flag-adapter.interface.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAEpF;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IACjC,yDAAyD;IACzD,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAE5B,wDAAwD;IACxD,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAE1E,4DAA4D;IAC5D,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAEtF,6CAA6C;IAC7C,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAE9F,iDAAiD;IACjD,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B"}
1
+ {"version":3,"file":"feature-flag-adapter.interface.d.ts","sourceRoot":"","sources":["../../src/adapters/feature-flag-adapter.interface.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAEpF;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IACjC,yDAAyD;IACzD,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAE5B,wDAAwD;IACxD,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAE1E,4DAA4D;IAC5D,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAEtF;;;;;;;;;;OAUG;IACH,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAE9F,iDAAiD;IACjD,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B"}
@@ -1,5 +1,5 @@
1
- import type { FeatureFlagAdapter } from './feature-flag-adapter.interface';
2
1
  import type { FeatureFlagContext, FeatureFlagVariant } from '../feature-flag.types';
2
+ import type { FeatureFlagAdapter } from './feature-flag-adapter.interface';
3
3
  /**
4
4
  * Static/in-memory feature flag adapter.
5
5
  * Flags are defined at construction time and don't change.
@@ -10,6 +10,11 @@ export declare class StaticFeatureFlagAdapter implements FeatureFlagAdapter {
10
10
  initialize(): Promise<void>;
11
11
  isEnabled(flagKey: string, _context: FeatureFlagContext): Promise<boolean>;
12
12
  getVariant(flagKey: string, _context: FeatureFlagContext): Promise<FeatureFlagVariant>;
13
+ /**
14
+ * A key this adapter has never been configured with is OMITTED rather than reported as
15
+ * `false`. Only then can the caller tell "the operator disabled it" from "nobody configured
16
+ * it" and apply the ref's `defaultValue` to the second case only.
17
+ */
13
18
  evaluateFlags(flagKeys: string[], context: FeatureFlagContext): Promise<Map<string, boolean>>;
14
19
  destroy(): Promise<void>;
15
20
  }
@@ -1 +1 @@
1
- {"version":3,"file":"static.adapter.d.ts","sourceRoot":"","sources":["../../src/adapters/static.adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAC3E,OAAO,KAAK,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAEpF;;;GAGG;AACH,qBAAa,wBAAyB,YAAW,kBAAkB;IACjE,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA+C;gBAEzD,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,GAAG,kBAAkB,CAAC;IAIzD,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAI3B,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,kBAAkB,GAAG,OAAO,CAAC,OAAO,CAAC;IAO1E,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAWtF,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAQ7F,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAG/B"}
1
+ {"version":3,"file":"static.adapter.d.ts","sourceRoot":"","sources":["../../src/adapters/static.adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AACpF,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kCAAkC,CAAC;AAE3E;;;GAGG;AACH,qBAAa,wBAAyB,YAAW,kBAAkB;IACjE,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA+C;gBAEzD,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,GAAG,kBAAkB,CAAC;IAIzD,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAI3B,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,kBAAkB,GAAG,OAAO,CAAC,OAAO,CAAC;IAO1E,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAW5F;;;;OAIG;IACG,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAS7F,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAG/B"}
package/esm/index.mjs CHANGED
@@ -246,24 +246,15 @@ import {
246
246
  DynamicPlugin,
247
247
  FlowHooksOf,
248
248
  FRONTMCP_CONTEXT,
249
- ListToolsHook,
249
+ FrontMcpContextStorage,
250
250
  ListResourcesHook,
251
+ ListResourceTemplatesHook,
252
+ ListToolsHook,
251
253
  Plugin,
252
254
  ProviderScope as ProviderScope2,
253
255
  ToolHook
254
256
  } from "@frontmcp/sdk";
255
257
 
256
- // plugins/plugin-feature-flags/src/feature-flag.symbols.ts
257
- var FeatureFlagAdapterToken = /* @__PURE__ */ Symbol(
258
- "plugin:feature-flags:adapter"
259
- );
260
- var FeatureFlagConfigToken = /* @__PURE__ */ Symbol(
261
- "plugin:feature-flags:config"
262
- );
263
- var FeatureFlagAccessorToken = /* @__PURE__ */ Symbol(
264
- "plugin:feature-flags:accessor"
265
- );
266
-
267
258
  // plugins/plugin-feature-flags/src/adapters/static.adapter.ts
268
259
  var StaticFeatureFlagAdapter = class {
269
260
  flags;
@@ -288,9 +279,15 @@ var StaticFeatureFlagAdapter = class {
288
279
  }
289
280
  return { ...flag };
290
281
  }
282
+ /**
283
+ * A key this adapter has never been configured with is OMITTED rather than reported as
284
+ * `false`. Only then can the caller tell "the operator disabled it" from "nobody configured
285
+ * it" and apply the ref's `defaultValue` to the second case only.
286
+ */
291
287
  async evaluateFlags(flagKeys, context) {
292
288
  const results = /* @__PURE__ */ new Map();
293
289
  for (const key of flagKeys) {
290
+ if (this.flags[key] === void 0) continue;
294
291
  results.set(key, await this.isEnabled(key, context));
295
292
  }
296
293
  return results;
@@ -299,6 +296,28 @@ var StaticFeatureFlagAdapter = class {
299
296
  }
300
297
  };
301
298
 
299
+ // plugins/plugin-feature-flags/src/feature-flag.context.ts
300
+ function buildFeatureFlagContext(ctx, config) {
301
+ if (!ctx) return {};
302
+ const userId = config.userIdResolver ? config.userIdResolver(ctx) : ctx.authInfo?.extra?.["sub"] ?? ctx.authInfo?.extra?.["userId"] ?? ctx.authInfo?.clientId;
303
+ return {
304
+ userId: userId ?? void 0,
305
+ sessionId: ctx.sessionId,
306
+ attributes: config.attributesResolver ? config.attributesResolver(ctx) : {}
307
+ };
308
+ }
309
+
310
+ // plugins/plugin-feature-flags/src/feature-flag.symbols.ts
311
+ var FeatureFlagAdapterToken = /* @__PURE__ */ Symbol(
312
+ "plugin:feature-flags:adapter"
313
+ );
314
+ var FeatureFlagConfigToken = /* @__PURE__ */ Symbol(
315
+ "plugin:feature-flags:config"
316
+ );
317
+ var FeatureFlagAccessorToken = /* @__PURE__ */ Symbol(
318
+ "plugin:feature-flags:accessor"
319
+ );
320
+
302
321
  // plugins/plugin-feature-flags/src/providers/feature-flag-accessor.provider.ts
303
322
  import { Provider, ProviderScope } from "@frontmcp/sdk";
304
323
  var FeatureFlagAccessor = class {
@@ -362,13 +381,7 @@ var FeatureFlagAccessor = class {
362
381
  * Build the FeatureFlagContext from the current FrontMcpContext.
363
382
  */
364
383
  buildContext() {
365
- const userId = this.config.userIdResolver ? this.config.userIdResolver(this.ctx) : this.ctx.authInfo?.extra?.["sub"] ?? this.ctx.authInfo?.extra?.["userId"] ?? this.ctx.authInfo?.clientId;
366
- const attributes = this.config.attributesResolver ? this.config.attributesResolver(this.ctx) : {};
367
- return {
368
- userId: userId ?? void 0,
369
- sessionId: this.ctx.sessionId,
370
- attributes
371
- };
384
+ return buildFeatureFlagContext(this.ctx, this.config);
372
385
  }
373
386
  };
374
387
  FeatureFlagAccessor = __decorateClass([
@@ -384,7 +397,10 @@ function createFeatureFlagAccessor(adapter, ctx, config) {
384
397
 
385
398
  // plugins/plugin-feature-flags/src/feature-flag.plugin.ts
386
399
  var ListPromptsHook = FlowHooksOf("prompts:list-prompts");
387
- var SearchSkillsHook = FlowHooksOf("skills:search");
400
+ var ReadResourceHook = FlowHooksOf("resources:read-resource");
401
+ var GetPromptHook = FlowHooksOf("prompts:get-prompt");
402
+ var CompleteHook = FlowHooksOf("completion:complete");
403
+ var FilterSkillsHook = FlowHooksOf("skills:filter");
388
404
  var FeatureFlagPlugin = class extends DynamicPlugin {
389
405
  options;
390
406
  constructor(options) {
@@ -419,6 +435,20 @@ var FeatureFlagPlugin = class extends DynamicPlugin {
419
435
  });
420
436
  flowCtx.state.set("resources", filtered);
421
437
  }
438
+ async filterListResourceTemplates(flowCtx) {
439
+ const { templates } = flowCtx.state;
440
+ if (!templates || templates.length === 0) return;
441
+ const flaggedTemplates = this.collectFlagRefs(templates, (item) => item.template.metadata.featureFlag);
442
+ if (flaggedTemplates.size === 0) return;
443
+ const adapter = this.get(FeatureFlagAdapterToken);
444
+ const flagResults = await this.batchEvaluateRefs(adapter, flaggedTemplates);
445
+ const filtered = templates.filter((item) => {
446
+ const ref = item.template.metadata.featureFlag;
447
+ if (!ref) return true;
448
+ return this.isRefEnabled(ref, flagResults);
449
+ });
450
+ flowCtx.state.set("templates", filtered);
451
+ }
422
452
  async filterListPrompts(flowCtx) {
423
453
  const { prompts } = flowCtx.state;
424
454
  if (!prompts || prompts.length === 0) return;
@@ -433,36 +463,57 @@ var FeatureFlagPlugin = class extends DynamicPlugin {
433
463
  });
434
464
  flowCtx.state.set("prompts", filtered);
435
465
  }
436
- async filterSearchSkills(flowCtx) {
437
- const { results } = flowCtx.state;
438
- if (!results || results.length === 0) return;
439
- const flaggedSkills = this.collectFlagRefs(results, (item) => item.metadata?.featureFlag);
466
+ async filterSkills(flowCtx) {
467
+ const { skills } = flowCtx.state;
468
+ if (!skills || skills.length === 0) return;
469
+ const flaggedSkills = this.collectFlagRefs(skills, (skill) => skill.metadata.featureFlag);
440
470
  if (flaggedSkills.size === 0) return;
441
471
  const adapter = this.get(FeatureFlagAdapterToken);
442
472
  const flagResults = await this.batchEvaluateRefs(adapter, flaggedSkills);
443
- const filtered = results.filter((item) => {
444
- const ref = item.metadata?.featureFlag;
473
+ const filtered = skills.filter((skill) => {
474
+ const ref = skill.metadata.featureFlag;
445
475
  if (!ref) return true;
446
476
  return this.isRefEnabled(ref, flagResults);
447
477
  });
448
- flowCtx.state.set("results", filtered);
478
+ flowCtx.state.set("skills", filtered);
449
479
  }
450
480
  async gateToolExecution(flowCtx) {
451
- const { tool } = flowCtx.state;
452
- if (!tool) return;
453
- const ref = tool.metadata?.featureFlag;
481
+ await this.gateEntryExecution("Tool", flowCtx.state.tool);
482
+ }
483
+ async gateResourceRead(flowCtx) {
484
+ await this.gateEntryExecution("Resource", flowCtx.state.resource);
485
+ }
486
+ async gatePromptGet(flowCtx) {
487
+ await this.gateEntryExecution("Prompt", flowCtx.state.prompt);
488
+ }
489
+ async gateCompletion(flowCtx) {
490
+ const { prompt, resource } = flowCtx.state;
491
+ if (prompt) await this.gateEntryExecution("Prompt", prompt);
492
+ if (resource) await this.gateEntryExecution("Resource", resource);
493
+ }
494
+ /**
495
+ * Shared execution gate for tools, resources and prompts.
496
+ *
497
+ * One implementation on purpose: the advisory existed because the tool path had a gate and
498
+ * the other two did not, and three copies would drift apart the same way.
499
+ */
500
+ async gateEntryExecution(kind, entry) {
501
+ if (!entry) return;
502
+ const metadata = entry.metadata;
503
+ const ref = metadata?.featureFlag;
454
504
  if (!ref) return;
455
- const adapter = this.get(FeatureFlagAdapterToken);
456
505
  const key = typeof ref === "string" ? ref : ref.key;
457
506
  const defaultValue = typeof ref === "object" ? ref.defaultValue ?? false : false;
458
507
  let enabled;
459
508
  try {
460
- enabled = await adapter.isEnabled(key, {});
509
+ const adapter = this.get(FeatureFlagAdapterToken);
510
+ const results = await adapter.evaluateFlags([key], this.currentFlagContext());
511
+ enabled = this.isRefEnabled(ref, results);
461
512
  } catch {
462
513
  enabled = defaultValue;
463
514
  }
464
515
  if (!enabled) {
465
- throw new Error(`Tool "${tool.metadata.name}" is disabled by feature flag "${key}"`);
516
+ throw new Error(`${kind} "${metadata?.name}" is disabled by feature flag "${key}"`);
466
517
  }
467
518
  }
468
519
  // ─────────────────────────────────────────────────────────────────────────
@@ -489,7 +540,24 @@ var FeatureFlagPlugin = class extends DynamicPlugin {
489
540
  */
490
541
  async batchEvaluateRefs(adapter, refs) {
491
542
  const keys = Array.from(refs.keys());
492
- return adapter.evaluateFlags(keys, {});
543
+ return adapter.evaluateFlags(keys, this.currentFlagContext());
544
+ }
545
+ /**
546
+ * The caller's evaluation context.
547
+ *
548
+ * Passing `{}` here asked the adapter "is this flag on for nobody in particular", which a
549
+ * targeted adapter can answer differently from "is it on for THIS caller" — enabling access
550
+ * the caller should not have. The context comes from the same `FrontMcpContext` the
551
+ * context-scoped accessor reads, so hooks and `this.featureFlags` agree.
552
+ */
553
+ currentFlagContext() {
554
+ try {
555
+ const ctx = this.get(FrontMcpContextStorage)?.getStore();
556
+ if (!ctx) return {};
557
+ return buildFeatureFlagContext(ctx, this.options);
558
+ } catch {
559
+ return {};
560
+ }
493
561
  }
494
562
  /**
495
563
  * Determine if a feature flag ref is enabled given adapter results.
@@ -498,9 +566,8 @@ var FeatureFlagPlugin = class extends DynamicPlugin {
498
566
  */
499
567
  isRefEnabled(ref, flagResults) {
500
568
  const key = typeof ref === "string" ? ref : ref.key;
501
- const adapterResult = flagResults.get(key);
502
569
  const defaultValue = typeof ref === "object" ? ref.defaultValue ?? false : false;
503
- if (adapterResult === true) return true;
570
+ if (flagResults.has(key)) return flagResults.get(key) === true;
504
571
  return defaultValue;
505
572
  }
506
573
  };
@@ -584,15 +651,27 @@ __decorateClass([
584
651
  __decorateClass([
585
652
  ListResourcesHook.Did("findResources", { priority: 50 })
586
653
  ], FeatureFlagPlugin.prototype, "filterListResources", 1);
654
+ __decorateClass([
655
+ ListResourceTemplatesHook.Did("findTemplates", { priority: 50 })
656
+ ], FeatureFlagPlugin.prototype, "filterListResourceTemplates", 1);
587
657
  __decorateClass([
588
658
  ListPromptsHook.Did("findPrompts", { priority: 50 })
589
659
  ], FeatureFlagPlugin.prototype, "filterListPrompts", 1);
590
660
  __decorateClass([
591
- SearchSkillsHook.Did("search", { priority: 50 })
592
- ], FeatureFlagPlugin.prototype, "filterSearchSkills", 1);
661
+ FilterSkillsHook.Did("filterSkills", { priority: 50 })
662
+ ], FeatureFlagPlugin.prototype, "filterSkills", 1);
593
663
  __decorateClass([
594
664
  ToolHook.Will("execute", { priority: 50 })
595
665
  ], FeatureFlagPlugin.prototype, "gateToolExecution", 1);
666
+ __decorateClass([
667
+ ReadResourceHook.Will("execute", { priority: 50 })
668
+ ], FeatureFlagPlugin.prototype, "gateResourceRead", 1);
669
+ __decorateClass([
670
+ GetPromptHook.Will("execute", { priority: 50 })
671
+ ], FeatureFlagPlugin.prototype, "gatePromptGet", 1);
672
+ __decorateClass([
673
+ CompleteHook.Will("complete", { priority: 50 })
674
+ ], FeatureFlagPlugin.prototype, "gateCompletion", 1);
596
675
  FeatureFlagPlugin = __decorateClass([
597
676
  Plugin({
598
677
  name: "feature-flags",
package/esm/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frontmcp/plugin-feature-flags",
3
- "version": "1.7.2",
3
+ "version": "1.8.1",
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.7.2"
52
+ "@frontmcp/sdk": "1.8.1"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "@launchdarkly/node-server-sdk": "^9.0.0 || ^10.0.0",
@@ -0,0 +1,12 @@
1
+ import type { FrontMcpContext } from '@frontmcp/sdk';
2
+ import type { FeatureFlagContext, FeatureFlagPluginOptions } from './feature-flag.types';
3
+ /**
4
+ * Build the adapter evaluation context for the current caller.
5
+ *
6
+ * Shared by the context-scoped accessor and by the plugin's list/gate hooks. Those hooks used
7
+ * to pass `{}`, which asks a targeted adapter "is this flag on for nobody in particular" — a
8
+ * question it can answer differently from "is it on for THIS caller", letting a gate allow
9
+ * access the caller should not have.
10
+ */
11
+ export declare function buildFeatureFlagContext(ctx: FrontMcpContext | undefined, config: Pick<FeatureFlagPluginOptions, 'userIdResolver' | 'attributesResolver'>): FeatureFlagContext;
12
+ //# sourceMappingURL=feature-flag.context.d.ts.map
@@ -0,0 +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,4 +1,4 @@
1
- import { DynamicPlugin, FlowCtxOf, ProviderType } from '@frontmcp/sdk';
1
+ import { DynamicPlugin, type FlowCtxOf, type ProviderType } from '@frontmcp/sdk';
2
2
  import type { FeatureFlagPluginOptions, FeatureFlagPluginOptionsInput } from './feature-flag.types';
3
3
  /**
4
4
  * FeatureFlagPlugin - Dynamic capability gating for FrontMCP.
@@ -35,20 +35,60 @@ export default class FeatureFlagPlugin extends DynamicPlugin<FeatureFlagPluginOp
35
35
  * Filter resources from list_resources based on feature flags.
36
36
  */
37
37
  filterListResources(flowCtx: FlowCtxOf<'resources:list-resources'>): Promise<void>;
38
+ /**
39
+ * Filter resource templates from resources/templates/list based on feature flags.
40
+ *
41
+ * GHSA-gf7p-j3hr-h5h4: templates were listed even when their flag was off.
42
+ */
43
+ filterListResourceTemplates(flowCtx: FlowCtxOf<'resources:list-resource-templates'>): Promise<void>;
38
44
  /**
39
45
  * Filter prompts from list_prompts based on feature flags.
40
46
  */
41
47
  filterListPrompts(flowCtx: any): Promise<void>;
42
48
  /**
43
- * Filter skills from skills:search based on feature flags.
44
- * Hooks after search stage, before finalize converts to output.
49
+ * Filter skills in the `skills:filter` flow, which every skill surface runs: MCP
50
+ * `skills/search`, `skills/list` and `skills/load`, SEP-2640 `skill://` resources, and the HTTP
51
+ * `/skills`, `/llm.txt` and `/llm_full.txt` endpoints. A skill removed here is absent from
52
+ * listings and not found when named.
53
+ *
54
+ * GHSA-gf7p-j3hr-h5h4: the only skill hook sat on `skills:search`, which the `skills/search`
55
+ * handler never ran and whose results carry no `featureFlag`, so no skill surface was gated.
45
56
  */
46
- filterSearchSkills(flowCtx: any): Promise<void>;
57
+ filterSkills(flowCtx: FlowCtxOf<'skills:filter'>): Promise<void>;
47
58
  /**
48
59
  * Execution gate: block direct tool/call when the tool's feature flag is off.
49
60
  * This prevents bypassing the list filter via direct tool invocation.
50
61
  */
51
62
  gateToolExecution(flowCtx: FlowCtxOf<'tools:call-tool'>): Promise<void>;
63
+ /**
64
+ * Execution gate: block resources/read when the resource's feature flag is off.
65
+ *
66
+ * GHSA-gf7p-j3hr-h5h4: only tools had this gate, so a flagged resource was merely absent
67
+ * from resources/list and still readable by URI. Hiding a capability from a listing is not
68
+ * the same as withholding it — clients cache listings and hold URIs from earlier sessions.
69
+ */
70
+ gateResourceRead(flowCtx: any): Promise<void>;
71
+ /**
72
+ * Execution gate: block prompts/get when the prompt's feature flag is off.
73
+ *
74
+ * The same gap as resources (GHSA-gf7p-j3hr-h5h4): filtering prompts/list left the prompt
75
+ * retrievable by name.
76
+ */
77
+ gatePromptGet(flowCtx: any): Promise<void>;
78
+ /**
79
+ * Execution gate: block completion/complete for a prompt or resource whose feature flag is off.
80
+ *
81
+ * GHSA-gf7p-j3hr-h5h4: completion looked the entry up and ran its argument completers without
82
+ * a gate, so a disabled resource template still suggested its values.
83
+ */
84
+ gateCompletion(flowCtx: any): Promise<void>;
85
+ /**
86
+ * Shared execution gate for tools, resources and prompts.
87
+ *
88
+ * One implementation on purpose: the advisory existed because the tool path had a gate and
89
+ * the other two did not, and three copies would drift apart the same way.
90
+ */
91
+ private gateEntryExecution;
52
92
  /**
53
93
  * Collect unique flag keys from items that have a featureFlag metadata.
54
94
  */
@@ -57,6 +97,15 @@ export default class FeatureFlagPlugin extends DynamicPlugin<FeatureFlagPluginOp
57
97
  * Batch evaluate all collected flag refs via the adapter.
58
98
  */
59
99
  private batchEvaluateRefs;
100
+ /**
101
+ * The caller's evaluation context.
102
+ *
103
+ * Passing `{}` here asked the adapter "is this flag on for nobody in particular", which a
104
+ * targeted adapter can answer differently from "is it on for THIS caller" — enabling access
105
+ * the caller should not have. The context comes from the same `FrontMcpContext` the
106
+ * context-scoped accessor reads, so hooks and `this.featureFlags` agree.
107
+ */
108
+ private currentFlagContext;
60
109
  /**
61
110
  * Determine if a feature flag ref is enabled given adapter results.
62
111
  * For object-style refs, `defaultValue` acts as a fallback when the adapter
@@ -1 +1 @@
1
- {"version":3,"file":"feature-flag.plugin.d.ts","sourceRoot":"","sources":["../src/feature-flag.plugin.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EACb,SAAS,EAOT,YAAY,EAEb,MAAM,eAAe,CAAC;AAEvB,OAAO,KAAK,EAAE,wBAAwB,EAAE,6BAA6B,EAAkB,MAAM,sBAAsB,CAAC;AAYpH;;;;;;;;;;;;;;;;;;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;;OAEG;IAEG,iBAAiB,CAAC,OAAO,EAAE,GAAG;IAmBpC;;;OAGG;IAEG,kBAAkB,CAAC,OAAO,EAAE,GAAG;IAmBrC;;;OAGG;IAEG,iBAAiB,CAAC,OAAO,EAAE,SAAS,CAAC,iBAAiB,CAAC;IA2B7D;;OAEG;IACH,OAAO,CAAC,eAAe;IAcvB;;OAEG;YACW,iBAAiB;IAQ/B;;;;OAIG;IACH,OAAO,CAAC,YAAY;CAOrB"}
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"}
@@ -77,6 +77,9 @@ declare global {
77
77
  interface ExtendFrontMcpResourceMetadata {
78
78
  featureFlag?: FeatureFlagRef;
79
79
  }
80
+ interface ExtendFrontMcpResourceTemplateMetadata {
81
+ featureFlag?: FeatureFlagRef;
82
+ }
80
83
  interface ExtendFrontMcpPromptMetadata {
81
84
  featureFlag?: FeatureFlagRef;
82
85
  }
@@ -1 +1 @@
1
- {"version":3,"file":"feature-flag.types.d.ts","sourceRoot":"","sources":["../src/feature-flag.types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"feature-flag.types.d.ts","sourceRoot":"","sources":["../src/feature-flag.types.ts"],"names":[],"mappings":"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"}
package/index.js CHANGED
@@ -257,17 +257,6 @@ module.exports = __toCommonJS(index_exports);
257
257
  // plugins/plugin-feature-flags/src/feature-flag.plugin.ts
258
258
  var import_sdk2 = require("@frontmcp/sdk");
259
259
 
260
- // plugins/plugin-feature-flags/src/feature-flag.symbols.ts
261
- var FeatureFlagAdapterToken = /* @__PURE__ */ Symbol(
262
- "plugin:feature-flags:adapter"
263
- );
264
- var FeatureFlagConfigToken = /* @__PURE__ */ Symbol(
265
- "plugin:feature-flags:config"
266
- );
267
- var FeatureFlagAccessorToken = /* @__PURE__ */ Symbol(
268
- "plugin:feature-flags:accessor"
269
- );
270
-
271
260
  // plugins/plugin-feature-flags/src/adapters/static.adapter.ts
272
261
  var StaticFeatureFlagAdapter = class {
273
262
  flags;
@@ -292,9 +281,15 @@ var StaticFeatureFlagAdapter = class {
292
281
  }
293
282
  return { ...flag };
294
283
  }
284
+ /**
285
+ * A key this adapter has never been configured with is OMITTED rather than reported as
286
+ * `false`. Only then can the caller tell "the operator disabled it" from "nobody configured
287
+ * it" and apply the ref's `defaultValue` to the second case only.
288
+ */
295
289
  async evaluateFlags(flagKeys, context) {
296
290
  const results = /* @__PURE__ */ new Map();
297
291
  for (const key of flagKeys) {
292
+ if (this.flags[key] === void 0) continue;
298
293
  results.set(key, await this.isEnabled(key, context));
299
294
  }
300
295
  return results;
@@ -303,6 +298,28 @@ var StaticFeatureFlagAdapter = class {
303
298
  }
304
299
  };
305
300
 
301
+ // plugins/plugin-feature-flags/src/feature-flag.context.ts
302
+ function buildFeatureFlagContext(ctx, config) {
303
+ if (!ctx) return {};
304
+ const userId = config.userIdResolver ? config.userIdResolver(ctx) : ctx.authInfo?.extra?.["sub"] ?? ctx.authInfo?.extra?.["userId"] ?? ctx.authInfo?.clientId;
305
+ return {
306
+ userId: userId ?? void 0,
307
+ sessionId: ctx.sessionId,
308
+ attributes: config.attributesResolver ? config.attributesResolver(ctx) : {}
309
+ };
310
+ }
311
+
312
+ // plugins/plugin-feature-flags/src/feature-flag.symbols.ts
313
+ var FeatureFlagAdapterToken = /* @__PURE__ */ Symbol(
314
+ "plugin:feature-flags:adapter"
315
+ );
316
+ var FeatureFlagConfigToken = /* @__PURE__ */ Symbol(
317
+ "plugin:feature-flags:config"
318
+ );
319
+ var FeatureFlagAccessorToken = /* @__PURE__ */ Symbol(
320
+ "plugin:feature-flags:accessor"
321
+ );
322
+
306
323
  // plugins/plugin-feature-flags/src/providers/feature-flag-accessor.provider.ts
307
324
  var import_sdk = require("@frontmcp/sdk");
308
325
  var FeatureFlagAccessor = class {
@@ -366,13 +383,7 @@ var FeatureFlagAccessor = class {
366
383
  * Build the FeatureFlagContext from the current FrontMcpContext.
367
384
  */
368
385
  buildContext() {
369
- const userId = this.config.userIdResolver ? this.config.userIdResolver(this.ctx) : this.ctx.authInfo?.extra?.["sub"] ?? this.ctx.authInfo?.extra?.["userId"] ?? this.ctx.authInfo?.clientId;
370
- const attributes = this.config.attributesResolver ? this.config.attributesResolver(this.ctx) : {};
371
- return {
372
- userId: userId ?? void 0,
373
- sessionId: this.ctx.sessionId,
374
- attributes
375
- };
386
+ return buildFeatureFlagContext(this.ctx, this.config);
376
387
  }
377
388
  };
378
389
  FeatureFlagAccessor = __decorateClass([
@@ -388,7 +399,10 @@ function createFeatureFlagAccessor(adapter, ctx, config) {
388
399
 
389
400
  // plugins/plugin-feature-flags/src/feature-flag.plugin.ts
390
401
  var ListPromptsHook = (0, import_sdk2.FlowHooksOf)("prompts:list-prompts");
391
- var SearchSkillsHook = (0, import_sdk2.FlowHooksOf)("skills:search");
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");
392
406
  var FeatureFlagPlugin = class extends import_sdk2.DynamicPlugin {
393
407
  options;
394
408
  constructor(options) {
@@ -423,6 +437,20 @@ var FeatureFlagPlugin = class extends import_sdk2.DynamicPlugin {
423
437
  });
424
438
  flowCtx.state.set("resources", filtered);
425
439
  }
440
+ async filterListResourceTemplates(flowCtx) {
441
+ const { templates } = flowCtx.state;
442
+ if (!templates || templates.length === 0) return;
443
+ const flaggedTemplates = this.collectFlagRefs(templates, (item) => item.template.metadata.featureFlag);
444
+ if (flaggedTemplates.size === 0) return;
445
+ const adapter = this.get(FeatureFlagAdapterToken);
446
+ const flagResults = await this.batchEvaluateRefs(adapter, flaggedTemplates);
447
+ const filtered = templates.filter((item) => {
448
+ const ref = item.template.metadata.featureFlag;
449
+ if (!ref) return true;
450
+ return this.isRefEnabled(ref, flagResults);
451
+ });
452
+ flowCtx.state.set("templates", filtered);
453
+ }
426
454
  async filterListPrompts(flowCtx) {
427
455
  const { prompts } = flowCtx.state;
428
456
  if (!prompts || prompts.length === 0) return;
@@ -437,36 +465,57 @@ var FeatureFlagPlugin = class extends import_sdk2.DynamicPlugin {
437
465
  });
438
466
  flowCtx.state.set("prompts", filtered);
439
467
  }
440
- async filterSearchSkills(flowCtx) {
441
- const { results } = flowCtx.state;
442
- if (!results || results.length === 0) return;
443
- const flaggedSkills = this.collectFlagRefs(results, (item) => item.metadata?.featureFlag);
468
+ async filterSkills(flowCtx) {
469
+ const { skills } = flowCtx.state;
470
+ if (!skills || skills.length === 0) return;
471
+ const flaggedSkills = this.collectFlagRefs(skills, (skill) => skill.metadata.featureFlag);
444
472
  if (flaggedSkills.size === 0) return;
445
473
  const adapter = this.get(FeatureFlagAdapterToken);
446
474
  const flagResults = await this.batchEvaluateRefs(adapter, flaggedSkills);
447
- const filtered = results.filter((item) => {
448
- const ref = item.metadata?.featureFlag;
475
+ const filtered = skills.filter((skill) => {
476
+ const ref = skill.metadata.featureFlag;
449
477
  if (!ref) return true;
450
478
  return this.isRefEnabled(ref, flagResults);
451
479
  });
452
- flowCtx.state.set("results", filtered);
480
+ flowCtx.state.set("skills", filtered);
453
481
  }
454
482
  async gateToolExecution(flowCtx) {
455
- const { tool } = flowCtx.state;
456
- if (!tool) return;
457
- const ref = tool.metadata?.featureFlag;
483
+ await this.gateEntryExecution("Tool", flowCtx.state.tool);
484
+ }
485
+ async gateResourceRead(flowCtx) {
486
+ await this.gateEntryExecution("Resource", flowCtx.state.resource);
487
+ }
488
+ async gatePromptGet(flowCtx) {
489
+ await this.gateEntryExecution("Prompt", flowCtx.state.prompt);
490
+ }
491
+ async gateCompletion(flowCtx) {
492
+ const { prompt, resource } = flowCtx.state;
493
+ if (prompt) await this.gateEntryExecution("Prompt", prompt);
494
+ if (resource) await this.gateEntryExecution("Resource", resource);
495
+ }
496
+ /**
497
+ * Shared execution gate for tools, resources and prompts.
498
+ *
499
+ * One implementation on purpose: the advisory existed because the tool path had a gate and
500
+ * the other two did not, and three copies would drift apart the same way.
501
+ */
502
+ async gateEntryExecution(kind, entry) {
503
+ if (!entry) return;
504
+ const metadata = entry.metadata;
505
+ const ref = metadata?.featureFlag;
458
506
  if (!ref) return;
459
- const adapter = this.get(FeatureFlagAdapterToken);
460
507
  const key = typeof ref === "string" ? ref : ref.key;
461
508
  const defaultValue = typeof ref === "object" ? ref.defaultValue ?? false : false;
462
509
  let enabled;
463
510
  try {
464
- enabled = await adapter.isEnabled(key, {});
511
+ const adapter = this.get(FeatureFlagAdapterToken);
512
+ const results = await adapter.evaluateFlags([key], this.currentFlagContext());
513
+ enabled = this.isRefEnabled(ref, results);
465
514
  } catch {
466
515
  enabled = defaultValue;
467
516
  }
468
517
  if (!enabled) {
469
- throw new Error(`Tool "${tool.metadata.name}" is disabled by feature flag "${key}"`);
518
+ throw new Error(`${kind} "${metadata?.name}" is disabled by feature flag "${key}"`);
470
519
  }
471
520
  }
472
521
  // ─────────────────────────────────────────────────────────────────────────
@@ -493,7 +542,24 @@ var FeatureFlagPlugin = class extends import_sdk2.DynamicPlugin {
493
542
  */
494
543
  async batchEvaluateRefs(adapter, refs) {
495
544
  const keys = Array.from(refs.keys());
496
- return adapter.evaluateFlags(keys, {});
545
+ return adapter.evaluateFlags(keys, this.currentFlagContext());
546
+ }
547
+ /**
548
+ * The caller's evaluation context.
549
+ *
550
+ * Passing `{}` here asked the adapter "is this flag on for nobody in particular", which a
551
+ * targeted adapter can answer differently from "is it on for THIS caller" — enabling access
552
+ * the caller should not have. The context comes from the same `FrontMcpContext` the
553
+ * context-scoped accessor reads, so hooks and `this.featureFlags` agree.
554
+ */
555
+ currentFlagContext() {
556
+ try {
557
+ const ctx = this.get(import_sdk2.FrontMcpContextStorage)?.getStore();
558
+ if (!ctx) return {};
559
+ return buildFeatureFlagContext(ctx, this.options);
560
+ } catch {
561
+ return {};
562
+ }
497
563
  }
498
564
  /**
499
565
  * Determine if a feature flag ref is enabled given adapter results.
@@ -502,9 +568,8 @@ var FeatureFlagPlugin = class extends import_sdk2.DynamicPlugin {
502
568
  */
503
569
  isRefEnabled(ref, flagResults) {
504
570
  const key = typeof ref === "string" ? ref : ref.key;
505
- const adapterResult = flagResults.get(key);
506
571
  const defaultValue = typeof ref === "object" ? ref.defaultValue ?? false : false;
507
- if (adapterResult === true) return true;
572
+ if (flagResults.has(key)) return flagResults.get(key) === true;
508
573
  return defaultValue;
509
574
  }
510
575
  };
@@ -588,15 +653,27 @@ __decorateClass([
588
653
  __decorateClass([
589
654
  import_sdk2.ListResourcesHook.Did("findResources", { priority: 50 })
590
655
  ], FeatureFlagPlugin.prototype, "filterListResources", 1);
656
+ __decorateClass([
657
+ import_sdk2.ListResourceTemplatesHook.Did("findTemplates", { priority: 50 })
658
+ ], FeatureFlagPlugin.prototype, "filterListResourceTemplates", 1);
591
659
  __decorateClass([
592
660
  ListPromptsHook.Did("findPrompts", { priority: 50 })
593
661
  ], FeatureFlagPlugin.prototype, "filterListPrompts", 1);
594
662
  __decorateClass([
595
- SearchSkillsHook.Did("search", { priority: 50 })
596
- ], FeatureFlagPlugin.prototype, "filterSearchSkills", 1);
663
+ FilterSkillsHook.Did("filterSkills", { priority: 50 })
664
+ ], FeatureFlagPlugin.prototype, "filterSkills", 1);
597
665
  __decorateClass([
598
666
  import_sdk2.ToolHook.Will("execute", { priority: 50 })
599
667
  ], FeatureFlagPlugin.prototype, "gateToolExecution", 1);
668
+ __decorateClass([
669
+ ReadResourceHook.Will("execute", { priority: 50 })
670
+ ], FeatureFlagPlugin.prototype, "gateResourceRead", 1);
671
+ __decorateClass([
672
+ GetPromptHook.Will("execute", { priority: 50 })
673
+ ], FeatureFlagPlugin.prototype, "gatePromptGet", 1);
674
+ __decorateClass([
675
+ CompleteHook.Will("complete", { priority: 50 })
676
+ ], FeatureFlagPlugin.prototype, "gateCompletion", 1);
600
677
  FeatureFlagPlugin = __decorateClass([
601
678
  (0, import_sdk2.Plugin)({
602
679
  name: "feature-flags",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frontmcp/plugin-feature-flags",
3
- "version": "1.7.2",
3
+ "version": "1.8.1",
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.7.2"
52
+ "@frontmcp/sdk": "1.8.1"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "@launchdarkly/node-server-sdk": "^9.0.0 || ^10.0.0",
@@ -1,6 +1,6 @@
1
1
  import { FrontMcpContext } from '@frontmcp/sdk';
2
2
  import type { FeatureFlagAdapter } from '../adapters/feature-flag-adapter.interface';
3
- import type { FeatureFlagVariant, FeatureFlagPluginOptions, FeatureFlagRef } from '../feature-flag.types';
3
+ import type { FeatureFlagPluginOptions, FeatureFlagRef, FeatureFlagVariant } from '../feature-flag.types';
4
4
  /**
5
5
  * Context-scoped accessor for feature flag evaluation.
6
6
  * Provides caching and context resolution from FrontMcpContext.
@@ -1 +1 @@
1
- {"version":3,"file":"feature-flag-accessor.provider.d.ts","sourceRoot":"","sources":["../../src/providers/feature-flag-accessor.provider.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAA2B,MAAM,eAAe,CAAC;AACzE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,4CAA4C,CAAC;AACrF,OAAO,KAAK,EAEV,kBAAkB,EAClB,wBAAwB,EACxB,cAAc,EACf,MAAM,uBAAuB,CAAC;AAE/B;;;GAGG;AACH,qBAKa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;IAC7C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAkB;IACtC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA2B;IAClD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA4D;gBAEtE,OAAO,EAAE,kBAAkB,EAAE,GAAG,EAAE,eAAe,EAAE,MAAM,EAAE,wBAAwB;IAM/F;;OAEG;IACG,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IA4B1E;;OAEG;IACG,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAK9D;;OAEG;IACG,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAKtE;;OAEG;IACG,UAAU,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,OAAO,CAAC;IAOvD;;OAEG;IACH,OAAO,CAAC,YAAY;CAerB;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,kBAAkB,EAC3B,GAAG,EAAE,eAAe,EACpB,MAAM,EAAE,wBAAwB,GAC/B,mBAAmB,CAErB"}
1
+ {"version":3,"file":"feature-flag-accessor.provider.d.ts","sourceRoot":"","sources":["../../src/providers/feature-flag-accessor.provider.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAA2B,MAAM,eAAe,CAAC;AAEzE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,4CAA4C,CAAC;AAErF,OAAO,KAAK,EAEV,wBAAwB,EACxB,cAAc,EACd,kBAAkB,EACnB,MAAM,uBAAuB,CAAC;AAE/B;;;GAGG;AACH,qBAKa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;IAC7C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAkB;IACtC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA2B;IAClD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA4D;gBAEtE,OAAO,EAAE,kBAAkB,EAAE,GAAG,EAAE,eAAe,EAAE,MAAM,EAAE,wBAAwB;IAM/F;;OAEG;IACG,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IA4B1E;;OAEG;IACG,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAK9D;;OAEG;IACG,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAKtE;;OAEG;IACG,UAAU,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,OAAO,CAAC;IAOvD;;OAEG;IACH,OAAO,CAAC,YAAY;CAGrB;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,kBAAkB,EAC3B,GAAG,EAAE,eAAe,EACpB,MAAM,EAAE,wBAAwB,GAC/B,mBAAmB,CAErB"}