Skip to main content
Glama

Bucket Feature Flags MCP Server

Official
by reflagcom
index.ts6.28 kB
import { ErrorCode, EvaluationContext, JsonValue, OpenFeatureEventEmitter, Paradigm, Provider, ResolutionDetails, ServerProviderStatus, StandardResolutionReasons, TrackingEventDetails, } from "@openfeature/server-sdk"; import { ClientOptions, Context as ReflagContext, ReflagClient, } from "@reflag/node-sdk"; type ProviderOptions = ClientOptions & { contextTranslator?: (context: EvaluationContext) => ReflagContext; }; export const defaultContextTranslator = ( context: EvaluationContext, ): ReflagContext => { const user = { id: context.targetingKey ?? context["userId"]?.toString(), name: context["name"]?.toString(), email: context["email"]?.toString(), avatar: context["avatar"]?.toString(), country: context["country"]?.toString(), }; const company = { id: context["companyId"]?.toString(), name: context["companyName"]?.toString(), avatar: context["companyAvatar"]?.toString(), plan: context["companyPlan"]?.toString(), }; return { user, company, }; }; export class ReflagNodeProvider implements Provider { public readonly events = new OpenFeatureEventEmitter(); private _client: ReflagClient; private contextTranslator: (context: EvaluationContext) => ReflagContext; public runsOn: Paradigm = "server"; public status: ServerProviderStatus = ServerProviderStatus.NOT_READY; public metadata = { name: "reflag-node", }; get client() { return this._client; } constructor({ contextTranslator, ...opts }: ProviderOptions) { this._client = new ReflagClient(opts); this.contextTranslator = contextTranslator ?? defaultContextTranslator; } public async initialize(): Promise<void> { await this._client.initialize(); this.status = ServerProviderStatus.READY; } private resolveFlag<T extends JsonValue>( flagKey: string, defaultValue: T, context: ReflagContext, resolveFn: ( feature: ReturnType<typeof this._client.getFlag>, ) => Promise<ResolutionDetails<T>>, ): Promise<ResolutionDetails<T>> { if (this.status !== ServerProviderStatus.READY) { return Promise.resolve({ value: defaultValue, reason: StandardResolutionReasons.ERROR, errorCode: ErrorCode.PROVIDER_NOT_READY, errorMessage: "Reflag client not initialized", }); } if (!context.user?.id) { return Promise.resolve({ value: defaultValue, reason: StandardResolutionReasons.ERROR, errorCode: ErrorCode.INVALID_CONTEXT, errorMessage: "At least a user ID is required", }); } const featureDefs = this._client.getFlagDefinitions(); if (featureDefs.some(({ key }) => key === flagKey)) { return resolveFn(this._client.getFlag(context, flagKey)); } return Promise.resolve({ value: defaultValue, reason: StandardResolutionReasons.ERROR, errorCode: ErrorCode.FLAG_NOT_FOUND, errorMessage: `Flag ${flagKey} not found`, }); } resolveBooleanEvaluation( flagKey: string, defaultValue: boolean, context: EvaluationContext, ): Promise<ResolutionDetails<boolean>> { return this.resolveFlag( flagKey, defaultValue, this.contextTranslator(context), (feature) => { return Promise.resolve({ value: feature.isEnabled, variant: feature.config?.key, reason: StandardResolutionReasons.TARGETING_MATCH, }); }, ); } resolveStringEvaluation( flagKey: string, defaultValue: string, context: EvaluationContext, ): Promise<ResolutionDetails<string>> { return this.resolveFlag( flagKey, defaultValue, this.contextTranslator(context), (feature) => { if (!feature.config.key) { return Promise.resolve({ value: defaultValue, reason: StandardResolutionReasons.DEFAULT, }); } return Promise.resolve({ value: feature.config.key as string, variant: feature.config.key, reason: StandardResolutionReasons.TARGETING_MATCH, }); }, ); } resolveNumberEvaluation( _flagKey: string, defaultValue: number, ): Promise<ResolutionDetails<number>> { return Promise.resolve({ value: defaultValue, reason: StandardResolutionReasons.ERROR, errorCode: ErrorCode.GENERAL, errorMessage: "Reflag doesn't support this method. Use `resolveObjectEvaluation` instead.", }); } resolveObjectEvaluation<T extends JsonValue>( flagKey: string, defaultValue: T, context: EvaluationContext, ): Promise<ResolutionDetails<T>> { return this.resolveFlag( flagKey, defaultValue, this.contextTranslator(context), (feature) => { const expType = typeof defaultValue; const payloadType = typeof feature.config.payload; if ( feature.config.payload === undefined || feature.config.payload === null || payloadType !== expType ) { return Promise.resolve({ value: defaultValue, variant: feature.config.key, reason: StandardResolutionReasons.ERROR, errorCode: ErrorCode.TYPE_MISMATCH, errorMessage: `Expected remote config payload of type \`${expType}\` but got \`${payloadType}\`.`, }); } return Promise.resolve({ value: feature.config.payload, variant: feature.config.key, reason: StandardResolutionReasons.TARGETING_MATCH, }); }, ); } track( trackingEventName: string, context?: EvaluationContext, trackingEventDetails?: TrackingEventDetails, ): void { const translatedContext = context ? this.contextTranslator(context) : undefined; const userId = translatedContext?.user?.id; if (!userId) { this._client.logger?.warn("No user ID provided for tracking event"); return; } void this._client.track(String(userId), trackingEventName, { attributes: trackingEventDetails, companyId: translatedContext?.company?.id?.toString(), }); } public async onClose(): Promise<void> { await this._client.flush(); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/reflagcom/bucket-javascript-sdk'

If you have feedback or need assistance with the MCP directory API, please join our Discord server