UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

200 lines • 10.8 kB
/** * Update Ruleset Tool - Individual Module * @description Updates complex rule configurations for flags and experiments * @since 2025-08-04 * @author Tool Modularization Team * * Migration Status: COMPLETED * Original Method: Delegates to manageEntityLifecycle * Complexity: HIGH * Dependencies: logger, errorMapper, manageEntityLifecycle, getRuleset, transformRuleForFeatureExperimentation */ /** * Creates the Update Ruleset tool with injected dependencies * @param deps - Injected dependencies (storage, logger, errorMapper, etc.) * @returns Tool definition with handler */ export function createUpdateRulesetTool(deps) { return { name: 'update_ruleset', requiresCache: true, category: 'management', description: `šŸ”§ UPDATE flag ruleset (rulesets belong to parent flags in specific environments) ⚔ HIERARCHY: project → flag → environment → ruleset → rules šŸ¤– DECISION PATTERN: • "Update flag rules in environment" → Use this tool • "Change flag behavior per environment" → Update its ruleset • "Add/remove targeting rules" → Modify flag's ruleset šŸ“Š REQUIRED CONTEXT: • project_id: Which project contains the flag • flag_key: Parent flag that owns the ruleset • environment_key: Specific environment (dev, staging, prod) • ruleset_data: JSON Patch operations for updates āš ļø JSON PATCH FORMAT: • op: "add" | "remove" | "replace" • path: "/rules/rule_key" | "/rule_priorities" | "/enabled" • value: Rule object or primitive value šŸ’” EXAMPLES: • Add rule: {"op": "add", "path": "/rules/new_rule", "value": {...}} • Enable ruleset: {"op": "replace", "path": "/enabled", "value": true} • Update priorities: {"op": "replace", "path": "/rule_priorities", "value": ["rule1", "rule2"]}`, handler: async (args) => { // Log the raw arguments to debug validation issues deps.logger.info("update_ruleset called with arguments", { tool: "update_ruleset", argsType: typeof args, argsKeys: args ? Object.keys(args) : "null", rawArgs: JSON.stringify(args, null, 2), }); if (!args || !args.project_id || !args.flag_key || !args.environment_key || !args.ruleset_data) { throw deps.errorMapper.toMCPError(new Error("Missing required parameters: project_id, flag_key, environment_key, and ruleset_data"), 'Missing required parameters'); } // VALIDATION: Support both object and array formats for ruleset_data if (typeof args.ruleset_data !== "object" || args.ruleset_data === null) { throw deps.errorMapper.toMCPError(new Error('ruleset_data must be an object or array. Object example: {"rules": {"rule_key": {...}}}. Array example: [{"op": "add", "path": "/rules/rule_key", "value": {...}}]'), 'Invalid ruleset_data format'); } try { // CRITICAL: First fetch the current ruleset to preserve existing fields like status const currentRuleset = await deps.getRuleset(args.project_id, args.flag_key, args.environment_key); deps.logger.info("update_ruleset: Fetched current ruleset state", { hasCurrentRuleset: !!currentRuleset, currentRuleCount: currentRuleset?.rules ? Object.keys(currentRuleset.rules).length : 0, currentStatus: currentRuleset?.status }); // Process the ruleset data using IntelligentPayloadParser let processedRulesetData = args.ruleset_data; try { // const { IntelligentPayloadParser } = await import('../../utils/IntelligentPayloadParser.js'); // For now, skip intelligent parsing in the extracted version // const parser = new IntelligentPayloadParser(); deps.logger.info("update_ruleset: Applying intelligent payload parsing to ruleset data", { dataType: Array.isArray(args.ruleset_data) ? "array" : "object", sampleData: JSON.stringify(args.ruleset_data).substring(0, 200), }); // Pass the data directly - let the parser figure out what it is // const parseResult = await parser.parsePayload( // args.ruleset_data, // { // entityType: "ruleset", // operation: "update", // platform: "feature", // enableFuzzyMatching: true, // } // ); const parseResult = { success: false }; // Placeholder for extracted version if (parseResult.success && parseResult.transformedPayload) { processedRulesetData = parseResult.transformedPayload; deps.logger.info("update_ruleset: Successfully transformed payload", { confidence: parseResult.confidence, transformations: parseResult.appliedTransformations, resultType: Array.isArray(processedRulesetData) ? "array" : typeof processedRulesetData, }); } } catch (parseError) { deps.logger.warn("update_ruleset: Intelligent parsing failed, using original payload", { error: parseError.message, }); } // Transform ruleset data to JSON Patch format if it's not already let patchData = processedRulesetData; if (!Array.isArray(patchData)) { // Convert object format to JSON Patch operations const patches = []; const rulesetObj = patchData; // If rules are provided, add them if (rulesetObj.rules) { if (Array.isArray(rulesetObj.rules)) { // Rules provided as array - extract each rule and use its key for (const rule of rulesetObj.rules) { if (rule.key || rule.name) { const ruleKey = rule.key || rule.name; const transformedRule = deps.transformRuleForFeatureExperimentation(rule); patches.push({ op: "add", path: `/rules/${ruleKey}`, value: transformedRule }); } } } else { // Rules provided as object for (const [key, rule] of Object.entries(rulesetObj.rules)) { const transformedRule = deps.transformRuleForFeatureExperimentation(rule); patches.push({ op: "add", path: `/rules/${key}`, value: transformedRule }); } } } // Add other fields as needed if ('enabled' in rulesetObj) { patches.push({ op: "replace", path: "/enabled", value: rulesetObj.enabled }); } patchData = patches; } // CRITICAL: Apply JSON Patch to current ruleset to maintain state let updatedRuleset = currentRuleset || { rules: {}, status: {} }; if (Array.isArray(patchData)) { // const { default: jsonpatch } = await import('fast-json-patch'); // For now, apply patches manually in extracted version const jsonpatch = { applyPatch: (doc, patches) => { // Simple implementation for common patch operations let newDoc = JSON.parse(JSON.stringify(doc)); for (const patch of patches) { const pathParts = patch.path.split('/').filter(Boolean); if (patch.op === 'add' || patch.op === 'replace') { let target = newDoc; for (let i = 0; i < pathParts.length - 1; i++) { if (!target[pathParts[i]]) target[pathParts[i]] = {}; target = target[pathParts[i]]; } target[pathParts[pathParts.length - 1]] = patch.value; } } return { newDocument: newDoc }; } }; deps.logger.info("update_ruleset: Applying JSON Patch operations", { patchCount: patchData.length, operations: patchData.map((p) => p.op) }); // Apply patches const patchResult = jsonpatch.applyPatch(updatedRuleset, patchData); updatedRuleset = patchResult.newDocument; } // Route to manageEntityLifecycle with the updated ruleset const result = await deps.manageEntityLifecycle('update', 'ruleset', updatedRuleset, undefined, args.project_id, { flag_key: args.flag_key, environment_key: args.environment_key }); return result; } catch (error) { deps.logger.error({ error: error.message, stack: error.stack }, 'UpdateRuleset: Tool execution failed'); throw deps.errorMapper.toMCPError(error, { operation: 'Update entity ruleset', tool: 'update_ruleset' }); } } }; } //# sourceMappingURL=UpdateRuleset.js.map