From bf9a1da95791741a68945483c4daa3d036c7db83 Mon Sep 17 00:00:00 2001 From: James Gill Date: Tue, 1 Sep 2026 08:59:02 +0100 Subject: [PATCH 1/8] feat(flags): expose the flag lifecycle endpoints as MCP tools The layer below added four endpoints that change one flag's state and take no request body. This turns them on for agents as `feature-flag-enable`, `feature-flag-disable`, `feature-flag-archive` and `feature-flag-unarchive`. Each tool's generated schema is the flag id and nothing else, so an agent cannot send `filters` through one. The descriptions say which field the action changes, state that the tool never accepts or replaces `filters`, and point at `scheduled-changes-create` for a future-dated change where that endpoint supports the operation. The disable description asks the agent to read the full definition and report linked-consumer impact when the user did not ask for the flag to be turned off. `update-feature-flag` keeps `active` and `archived` for combined edits. Its description now names the dedicated tool when one matches the request. `disable` joins the trailing-action list so the compact domain index renders `feature-flag` rather than `feature-flag-disable`. Eval tasks cover tool selection for each action, and that a targeting edit still picks `update-feature-flag`. Their success criteria judge the tool chosen and what the agent reports, so they score without a seeded fixture flag. Co-Authored-By: Claude Opus 5 (1M context) --- products/feature_flags/mcp/tools.yaml | 140 ++++++++++++++++-- services/mcp/evals/benchmark/tasks.yaml | 57 +++++++ .../schema/generated-tool-definitions.json | 58 +++++++- services/mcp/schema/tool-definitions-all.json | 58 +++++++- .../mcp/src/generated/feature_flags/api.ts | 71 ++++++++- services/mcp/src/lib/instructions.ts | 1 + .../tool-schemas/feature-flag-archive.json | 11 ++ .../tool-schemas/feature-flag-disable.json | 11 ++ .../tool-schemas/feature-flag-enable.json | 11 ++ .../tool-schemas/feature-flag-unarchive.json | 11 ++ .../unit/feature-flag-lifecycle-tools.test.ts | 62 ++++++++ services/mcp/tests/unit/instructions.test.ts | 1 + 12 files changed, 479 insertions(+), 13 deletions(-) create mode 100644 services/mcp/tests/unit/__snapshots__/tool-schemas/feature-flag-archive.json create mode 100644 services/mcp/tests/unit/__snapshots__/tool-schemas/feature-flag-disable.json create mode 100644 services/mcp/tests/unit/__snapshots__/tool-schemas/feature-flag-enable.json create mode 100644 services/mcp/tests/unit/__snapshots__/tool-schemas/feature-flag-unarchive.json create mode 100644 services/mcp/tests/unit/feature-flag-lifecycle-tools.test.ts diff --git a/products/feature_flags/mcp/tools.yaml b/products/feature_flags/mcp/tools.yaml index 0813942c413c..fc6ee86f734f 100644 --- a/products/feature_flags/mcp/tools.yaml +++ b/products/feature_flags/mcp/tools.yaml @@ -94,13 +94,103 @@ tools: enabled: false feature-flag-archive: operation: feature_flags_archive_create - enabled: false + enabled: true + scopes: + - feature_flag:write + annotations: + readOnly: false + destructive: false + idempotent: true + enrich_url: '{id}' + title: Archive feature flag + description: > + Archive a feature flag, hiding it from the default flag list. Requires the flag's numeric ID; use + `feature-flag-get-definition-by-key` to get it from a key, or `feature-flag-get-all` to search by key or + name. + + + Sets `archived` to true. An archived flag must be disabled, so an enabled flag is turned off in the same + call. Nothing else changes: this tool never accepts or replaces the `filters` object, and linked experiment + and survey history is preserved. There is no request body. + + + Returns 400 when the flag is enabled and other active flags depend on it. Returns 409 when an approval + policy gates disabling. Archiving an already-archived flag succeeds and changes nothing, so retrying is + safe. + + + Use `feature-flag-unarchive` to put the flag back in the list, or `delete-feature-flag` when the user wants + it gone rather than tidied away. + param_overrides: + id: + cast: string-int feature-flag-disable: operation: feature_flags_disable_create - enabled: false + enabled: true + scopes: + - feature_flag:write + annotations: + readOnly: false + destructive: false + idempotent: true + enrich_url: '{id}' + title: Disable feature flag + description: > + Turn a feature flag off. Requires the flag's numeric ID; use `feature-flag-get-definition-by-key` to get it + from a key, or `feature-flag-get-all` to search by key or name. + + + Sets `active` to false and changes nothing else. This tool never accepts or replaces the `filters` object, + so targeting, rollout percentages, variants and payloads cannot change. There is no request body. + + + A disabled flag stops evaluating everywhere, so every caller falls back to its own default. When the user + did not explicitly ask for the flag to be turned off, read the full definition with + `feature-flag-get-definition` first and tell them what else this affects: linked experiments, surveys, early + access features and session replay settings all appear there. + + + Returns 400 when other active flags depend on this one; `feature-flags-dependent-flags-retrieve` lists them. + Returns 409 when an approval policy gates disabling. Disabling an already-disabled flag succeeds and changes + nothing, so retrying is safe. + + + To turn the flag off at a future time instead, use `scheduled-changes-create` with the `update_status` + operation. + param_overrides: + id: + cast: string-int feature-flag-enable: operation: feature_flags_enable_create - enabled: false + enabled: true + scopes: + - feature_flag:write + annotations: + readOnly: false + destructive: false + idempotent: true + enrich_url: '{id}' + title: Enable feature flag + description: > + Turn a feature flag on. Requires the flag's numeric ID; use `feature-flag-get-definition-by-key` to get it + from a key, or `feature-flag-get-all` to search by key or name. + + + Sets `active` to true and changes nothing else. This tool never accepts or replaces the `filters` object, so + targeting, rollout percentages, variants and payloads cannot change. There is no request body. To change who + the flag is served to, use `update-feature-flag`. + + + Returns 400 when the flag is archived (call `feature-flag-unarchive` first) or when a flag it depends on is + disabled. Returns 409 when an approval policy gates enabling. Enabling an already-enabled flag succeeds and + changes nothing, so retrying is safe. + + + To turn the flag on at a future time instead, use `scheduled-changes-create` with the `update_status` + operation. + param_overrides: + id: + cast: string-int feature-flag-get-all: operation: feature_flags_list enabled: true @@ -153,7 +243,29 @@ tools: cast: string-int feature-flag-unarchive: operation: feature_flags_unarchive_create - enabled: false + enabled: true + scopes: + - feature_flag:write + annotations: + readOnly: false + destructive: false + idempotent: true + enrich_url: '{id}' + title: Unarchive feature flag + description: > + Restore an archived feature flag to the default flag list. Requires the flag's numeric ID; pass + `archived=true` to `feature-flag-get-all` to find archived flags. + + + Sets `archived` to false and changes nothing else. This tool never accepts or replaces the `filters` object. + There is no request body. + + + The flag stays turned off. Call `feature-flag-enable` when the user also wants it serving again. Unarchiving + a flag that is not archived succeeds and changes nothing, so retrying is safe. + param_overrides: + id: + cast: string-int feature-flags-activity-retrieve: operation: feature_flags_activity_retrieve enabled: true @@ -499,12 +611,20 @@ tools: enrich_url: '{id}' title: Update feature flag description: > - Update a feature flag by numeric ID (partial update — only the fields you include are changed). Call when - the user asks to enable or disable a flag, change its rollout percentage or release conditions, edit - variants or payloads, or update its description. Rollout conditions, variants, and payloads live in the - structured `filters` param, and sending `filters` replaces the whole object — read the flag first with - `feature-flag-get-definition` and merge your change. To apply a change at a future time instead, use - `scheduled-changes-create`. Use `feature-flag-get-all` to find the numeric ID from a key. + Update a feature flag by numeric ID (partial update: only the fields you include are changed). Use this for + edits that have no dedicated tool, such as release conditions, rollout percentages, variants, payloads, tags + and the description. + + + Prefer a dedicated tool whenever the request matches one. `feature-flag-enable`, `feature-flag-disable`, + `feature-flag-archive` and `feature-flag-unarchive` each change one state field, take no `filters`, and + cannot lose a concurrent edit. + + + Rollout conditions, variants, and payloads live in the structured `filters` param, and sending `filters` + replaces the whole object, so read the flag first with `feature-flag-get-definition` and merge your change. + To apply a change at a future time instead, use `scheduled-changes-create`. Use `feature-flag-get-all` to + find the numeric ID from a key. include_params: - key - name diff --git a/services/mcp/evals/benchmark/tasks.yaml b/services/mcp/evals/benchmark/tasks.yaml index dbcac1446f52..903af3a88ecb 100644 --- a/services/mcp/evals/benchmark/tasks.yaml +++ b/services/mcp/evals/benchmark/tasks.yaml @@ -212,6 +212,63 @@ tasks: acceptable_tools: [feature-flag-get-all, feature-flag-get-definition] success_criteria: "Resolves the flag by its key ('mcp-analytics') directly — does not guess a numeric id." + # The lifecycle tasks below act on a flag the benchmark project owns, so an agent-mode + # run flips only that flag's state. They score tool selection whether or not + # `mcp-eval-lifecycle` exists, because a missing flag must produce a clear answer rather + # than a fallback to the generic update tool. Seed it (enabled, not archived) to also + # exercise the state change itself. + - id: flag-enable-by-key + category: feature-flags + intent: 'Turn on the mcp-eval-lifecycle feature flag.' + expected_tools: [feature-flag-enable] + acceptable_tools: [feature-flag-get-definition-by-key, feature-flag-get-all, feature-flag-get-definition] + success_criteria: > + Calls feature-flag-enable rather than a generic update, and sends no filters payload in any call. If the + flag does not exist, says so rather than reaching for update-feature-flag. + + - id: flag-disable-reports-impact + category: feature-flags + intent: "We're seeing errors from the mcp-eval-lifecycle flag. Switch it off." + expected_tools: [feature-flag-disable] + acceptable_tools: + [ + feature-flag-get-definition-by-key, + feature-flag-get-all, + feature-flag-get-definition, + feature-flags-dependent-flags-retrieve, + ] + success_criteria: > + Resolves the key to an id and calls feature-flag-disable with no filters payload. Reads the definition first + and says what else the flag is wired into (linked experiments, surveys, replay settings), or that nothing + is. If the flag does not exist, says so rather than reaching for update-feature-flag. + + - id: flag-archive-completed + category: feature-flags + intent: 'The mcp-eval-lifecycle rollout is finished. Archive the flag so it stops cluttering the list.' + expected_tools: [feature-flag-archive] + acceptable_tools: [feature-flag-get-definition-by-key, feature-flag-get-all, feature-flag-get-definition] + success_criteria: > + Calls feature-flag-archive with no filters payload, and tells the user archiving also turns the flag off. + If the flag does not exist, says so rather than reaching for update-feature-flag. + + - id: flag-unarchive-stays-off + category: feature-flags + intent: 'Bring the mcp-eval-lifecycle flag back into the flag list, I need to look at it again.' + expected_tools: [feature-flag-unarchive] + acceptable_tools: [feature-flag-get-all, feature-flag-get-definition-by-key, feature-flag-get-definition] + success_criteria: > + Calls feature-flag-unarchive with no filters payload and does not enable it, saying the flag is still turned + off. If the flag does not exist, says so rather than reaching for update-feature-flag. + + - id: flag-targeting-edit-uses-update + category: feature-flags + intent: 'Change the mcp-eval-lifecycle flag so it only applies to users on the @example.com domain.' + expected_tools: [update-feature-flag] + acceptable_tools: [feature-flag-get-definition-by-key, feature-flag-get-definition, feature-flag-get-all] + success_criteria: > + Reads the current definition first, then edits targeting through update-feature-flag. Does not reach for a + lifecycle tool, which cannot change targeting. If the flag does not exist, says so. + # --- session-replay ---------------------------------------------------------- - id: replay-rage-clicks category: session-replay diff --git a/services/mcp/schema/generated-tool-definitions.json b/services/mcp/schema/generated-tool-definitions.json index 7833bfb1f146..35faefa1d6bc 100644 --- a/services/mcp/schema/generated-tool-definitions.json +++ b/services/mcp/schema/generated-tool-definitions.json @@ -4821,6 +4821,48 @@ "readOnlyHint": true } }, + "feature-flag-archive": { + "description": "Archive a feature flag, hiding it from the default flag list. Requires the flag's numeric ID; use `feature-flag-get-definition-by-key` to get it from a key, or `feature-flag-get-all` to search by key or name.\n\nSets `archived` to true. An archived flag must be disabled, so an enabled flag is turned off in the same call. Nothing else changes: this tool never accepts or replaces the `filters` object, and linked experiment and survey history is preserved. There is no request body.\n\nReturns 400 when the flag is enabled and other active flags depend on it. Returns 409 when an approval policy gates disabling. Archiving an already-archived flag succeeds and changes nothing, so retrying is safe.\n\nUse `feature-flag-unarchive` to put the flag back in the list, or `delete-feature-flag` when the user wants it gone rather than tidied away.", + "category": "Feature flags", + "feature": "flags", + "summary": "Archive feature flag", + "title": "Archive feature flag", + "required_scopes": ["feature_flag:write"], + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true, + "readOnlyHint": false + } + }, + "feature-flag-disable": { + "description": "Turn a feature flag off. Requires the flag's numeric ID; use `feature-flag-get-definition-by-key` to get it from a key, or `feature-flag-get-all` to search by key or name.\n\nSets `active` to false and changes nothing else. This tool never accepts or replaces the `filters` object, so targeting, rollout percentages, variants and payloads cannot change. There is no request body.\n\nA disabled flag stops evaluating everywhere, so every caller falls back to its own default. When the user did not explicitly ask for the flag to be turned off, read the full definition with `feature-flag-get-definition` first and tell them what else this affects: linked experiments, surveys, early access features and session replay settings all appear there.\n\nReturns 400 when other active flags depend on this one; `feature-flags-dependent-flags-retrieve` lists them. Returns 409 when an approval policy gates disabling. Disabling an already-disabled flag succeeds and changes nothing, so retrying is safe.\n\nTo turn the flag off at a future time instead, use `scheduled-changes-create` with the `update_status` operation.", + "category": "Feature flags", + "feature": "flags", + "summary": "Disable feature flag", + "title": "Disable feature flag", + "required_scopes": ["feature_flag:write"], + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true, + "readOnlyHint": false + } + }, + "feature-flag-enable": { + "description": "Turn a feature flag on. Requires the flag's numeric ID; use `feature-flag-get-definition-by-key` to get it from a key, or `feature-flag-get-all` to search by key or name.\n\nSets `active` to true and changes nothing else. This tool never accepts or replaces the `filters` object, so targeting, rollout percentages, variants and payloads cannot change. There is no request body. To change who the flag is served to, use `update-feature-flag`.\n\nReturns 400 when the flag is archived (call `feature-flag-unarchive` first) or when a flag it depends on is disabled. Returns 409 when an approval policy gates enabling. Enabling an already-enabled flag succeeds and changes nothing, so retrying is safe.\n\nTo turn the flag on at a future time instead, use `scheduled-changes-create` with the `update_status` operation.", + "category": "Feature flags", + "feature": "flags", + "summary": "Enable feature flag", + "title": "Enable feature flag", + "required_scopes": ["feature_flag:write"], + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true, + "readOnlyHint": false + } + }, "feature-flag-get-all": { "description": "Get feature flags in the current project. Supports list filters including search by feature flag key or name (case-insensitive), then use the returned ID for get/update/delete tools.", "category": "Feature flags", @@ -4849,6 +4891,20 @@ "readOnlyHint": true } }, + "feature-flag-unarchive": { + "description": "Restore an archived feature flag to the default flag list. Requires the flag's numeric ID; pass `archived=true` to `feature-flag-get-all` to find archived flags.\n\nSets `archived` to false and changes nothing else. This tool never accepts or replaces the `filters` object. There is no request body.\n\nThe flag stays turned off. Call `feature-flag-enable` when the user also wants it serving again. Unarchiving a flag that is not archived succeeds and changes nothing, so retrying is safe.", + "category": "Feature flags", + "feature": "flags", + "summary": "Unarchive feature flag", + "title": "Unarchive feature flag", + "required_scopes": ["feature_flag:write"], + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true, + "readOnlyHint": false + } + }, "feature-flags-activity-retrieve": { "description": "Get the audit trail for a specific feature flag by ID. Returns a paginated list of changes including who made changes, what was changed, and when. Use limit and page query params for pagination.", "category": "Feature flags", @@ -10782,7 +10838,7 @@ "feature_flag": "tasks" }, "update-feature-flag": { - "description": "Update a feature flag by numeric ID (partial update — only the fields you include are changed). Call when the user asks to enable or disable a flag, change its rollout percentage or release conditions, edit variants or payloads, or update its description. Rollout conditions, variants, and payloads live in the structured `filters` param, and sending `filters` replaces the whole object — read the flag first with `feature-flag-get-definition` and merge your change. To apply a change at a future time instead, use `scheduled-changes-create`. Use `feature-flag-get-all` to find the numeric ID from a key.", + "description": "Update a feature flag by numeric ID (partial update: only the fields you include are changed). Use this for edits that have no dedicated tool, such as release conditions, rollout percentages, variants, payloads, tags and the description.\n\nPrefer a dedicated tool whenever the request matches one. `feature-flag-enable`, `feature-flag-disable`, `feature-flag-archive` and `feature-flag-unarchive` each change one state field, take no `filters`, and cannot lose a concurrent edit.\n\nRollout conditions, variants, and payloads live in the structured `filters` param, and sending `filters` replaces the whole object, so read the flag first with `feature-flag-get-definition` and merge your change. To apply a change at a future time instead, use `scheduled-changes-create`. Use `feature-flag-get-all` to find the numeric ID from a key.", "category": "Feature flags", "feature": "flags", "summary": "Update feature flag", diff --git a/services/mcp/schema/tool-definitions-all.json b/services/mcp/schema/tool-definitions-all.json index d68ad53851b3..01b77f3abdaa 100644 --- a/services/mcp/schema/tool-definitions-all.json +++ b/services/mcp/schema/tool-definitions-all.json @@ -4976,6 +4976,48 @@ "readOnlyHint": true } }, + "feature-flag-archive": { + "description": "Archive a feature flag, hiding it from the default flag list. Requires the flag's numeric ID; use `feature-flag-get-definition-by-key` to get it from a key, or `feature-flag-get-all` to search by key or name.\n\nSets `archived` to true. An archived flag must be disabled, so an enabled flag is turned off in the same call. Nothing else changes: this tool never accepts or replaces the `filters` object, and linked experiment and survey history is preserved. There is no request body.\n\nReturns 400 when the flag is enabled and other active flags depend on it. Returns 409 when an approval policy gates disabling. Archiving an already-archived flag succeeds and changes nothing, so retrying is safe.\n\nUse `feature-flag-unarchive` to put the flag back in the list, or `delete-feature-flag` when the user wants it gone rather than tidied away.", + "category": "Feature flags", + "feature": "flags", + "summary": "Archive feature flag", + "title": "Archive feature flag", + "required_scopes": ["feature_flag:write"], + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true, + "readOnlyHint": false + } + }, + "feature-flag-disable": { + "description": "Turn a feature flag off. Requires the flag's numeric ID; use `feature-flag-get-definition-by-key` to get it from a key, or `feature-flag-get-all` to search by key or name.\n\nSets `active` to false and changes nothing else. This tool never accepts or replaces the `filters` object, so targeting, rollout percentages, variants and payloads cannot change. There is no request body.\n\nA disabled flag stops evaluating everywhere, so every caller falls back to its own default. When the user did not explicitly ask for the flag to be turned off, read the full definition with `feature-flag-get-definition` first and tell them what else this affects: linked experiments, surveys, early access features and session replay settings all appear there.\n\nReturns 400 when other active flags depend on this one; `feature-flags-dependent-flags-retrieve` lists them. Returns 409 when an approval policy gates disabling. Disabling an already-disabled flag succeeds and changes nothing, so retrying is safe.\n\nTo turn the flag off at a future time instead, use `scheduled-changes-create` with the `update_status` operation.", + "category": "Feature flags", + "feature": "flags", + "summary": "Disable feature flag", + "title": "Disable feature flag", + "required_scopes": ["feature_flag:write"], + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true, + "readOnlyHint": false + } + }, + "feature-flag-enable": { + "description": "Turn a feature flag on. Requires the flag's numeric ID; use `feature-flag-get-definition-by-key` to get it from a key, or `feature-flag-get-all` to search by key or name.\n\nSets `active` to true and changes nothing else. This tool never accepts or replaces the `filters` object, so targeting, rollout percentages, variants and payloads cannot change. There is no request body. To change who the flag is served to, use `update-feature-flag`.\n\nReturns 400 when the flag is archived (call `feature-flag-unarchive` first) or when a flag it depends on is disabled. Returns 409 when an approval policy gates enabling. Enabling an already-enabled flag succeeds and changes nothing, so retrying is safe.\n\nTo turn the flag on at a future time instead, use `scheduled-changes-create` with the `update_status` operation.", + "category": "Feature flags", + "feature": "flags", + "summary": "Enable feature flag", + "title": "Enable feature flag", + "required_scopes": ["feature_flag:write"], + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true, + "readOnlyHint": false + } + }, "feature-flag-get-all": { "description": "Get feature flags in the current project. Supports list filters including search by feature flag key or name (case-insensitive), then use the returned ID for get/update/delete tools.", "category": "Feature flags", @@ -5018,6 +5060,20 @@ "readOnlyHint": true } }, + "feature-flag-unarchive": { + "description": "Restore an archived feature flag to the default flag list. Requires the flag's numeric ID; pass `archived=true` to `feature-flag-get-all` to find archived flags.\n\nSets `archived` to false and changes nothing else. This tool never accepts or replaces the `filters` object. There is no request body.\n\nThe flag stays turned off. Call `feature-flag-enable` when the user also wants it serving again. Unarchiving a flag that is not archived succeeds and changes nothing, so retrying is safe.", + "category": "Feature flags", + "feature": "flags", + "summary": "Unarchive feature flag", + "title": "Unarchive feature flag", + "required_scopes": ["feature_flag:write"], + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true, + "readOnlyHint": false + } + }, "feature-flags-activity-retrieve": { "description": "Get the audit trail for a specific feature flag by ID. Returns a paginated list of changes including who made changes, what was changed, and when. Use limit and page query params for pagination.", "category": "Feature flags", @@ -11410,7 +11466,7 @@ "feature_flag": "tasks" }, "update-feature-flag": { - "description": "Update a feature flag by numeric ID (partial update — only the fields you include are changed). Call when the user asks to enable or disable a flag, change its rollout percentage or release conditions, edit variants or payloads, or update its description. Rollout conditions, variants, and payloads live in the structured `filters` param, and sending `filters` replaces the whole object — read the flag first with `feature-flag-get-definition` and merge your change. To apply a change at a future time instead, use `scheduled-changes-create`. Use `feature-flag-get-all` to find the numeric ID from a key.", + "description": "Update a feature flag by numeric ID (partial update: only the fields you include are changed). Use this for edits that have no dedicated tool, such as release conditions, rollout percentages, variants, payloads, tags and the description.\n\nPrefer a dedicated tool whenever the request matches one. `feature-flag-enable`, `feature-flag-disable`, `feature-flag-archive` and `feature-flag-unarchive` each change one state field, take no `filters`, and cannot lose a concurrent edit.\n\nRollout conditions, variants, and payloads live in the structured `filters` param, and sending `filters` replaces the whole object, so read the flag first with `feature-flag-get-definition` and merge your change. To apply a change at a future time instead, use `scheduled-changes-create`. Use `feature-flag-get-all` to find the numeric ID from a key.", "category": "Feature flags", "feature": "flags", "summary": "Update feature flag", diff --git a/services/mcp/src/generated/feature_flags/api.ts b/services/mcp/src/generated/feature_flags/api.ts index 202050e600fb..9ecc708b50f1 100644 --- a/services/mcp/src/generated/feature_flags/api.ts +++ b/services/mcp/src/generated/feature_flags/api.ts @@ -3,7 +3,7 @@ * MCP service uses these Zod schemas for generated tool handlers. * To regenerate: hogli build:openapi * - * PostHog API - MCP 21 enabled ops + * PostHog API - MCP 25 enabled ops * OpenAPI spec version: 1.0.0 */ import * as zod from 'zod' @@ -891,6 +891,24 @@ export const FeatureFlagsActivityRetrieveQueryParams = () => zod.object({ page: zod.number().min(1).default(featureFlagsActivityRetrieveQueryPageDefault).describe('Page number'), }) +/** + * Archive a feature flag, hiding it from the default flag list. + * + * Sets `archived` to true. An archived flag must be disabled, so an enabled flag also gets + * `active` set to false in the same write. Targeting, variants and payloads are left as + * they are, and linked experiment and survey history is preserved. Archiving an enabled + * flag is refused when other active flags depend on it. An already-archived flag is + * returned unchanged. + */ +export const FeatureFlagsArchiveCreateParams = /* @__PURE__ */ zod.object({ + id: zod.number().describe('A unique integer value identifying this feature flag.'), + project_id: zod + .string() + .describe( + "Project ID of the project you're trying to access. To find the ID of the project, make a call to \/api\/projects\/." + ), +}) + /** * Get other active flags that depend on this flag. */ @@ -903,6 +921,42 @@ export const FeatureFlagsDependentFlagsListParams = () => zod.object({ ), }) +/** + * Disable a feature flag. + * + * Sets `active` to false and changes nothing else. Targeting, variants, payloads, tags and + * archived state are left as they are. Refused when other active flags depend on this one. + * An already-disabled flag is returned unchanged. + * + * A disabled flag stops evaluating for every consumer, including a linked experiment or a + * session replay setting. Read the full definition first to report that impact. + */ +export const FeatureFlagsDisableCreateParams = /* @__PURE__ */ zod.object({ + id: zod.number().describe('A unique integer value identifying this feature flag.'), + project_id: zod + .string() + .describe( + "Project ID of the project you're trying to access. To find the ID of the project, make a call to \/api\/projects\/." + ), +}) + +/** + * Enable a feature flag. + * + * Sets `active` to true and changes nothing else. Targeting, variants, payloads, tags and + * archived state are left as they are. An archived flag is refused: unarchive it first. A + * flag whose own flag dependencies are disabled is also refused. An already-enabled flag + * is returned unchanged. + */ +export const FeatureFlagsEnableCreateParams = /* @__PURE__ */ zod.object({ + id: zod.number().describe('A unique integer value identifying this feature flag.'), + project_id: zod + .string() + .describe( + "Project ID of the project you're trying to access. To find the ID of the project, make a call to \/api\/projects\/." + ), +}) + /** * Create, read, update and delete feature flags. [See docs](https://posthog.com/docs/feature-flags) for more information on feature flags. * @@ -951,6 +1005,21 @@ export const FeatureFlagsTestEvaluationCreateBody = () => zod.object({ .describe('Groups for feature flag evaluation (JSON object, defaults to empty dict)'), }) +/** + * Restore an archived feature flag to the default flag list. + * + * Sets `archived` to false and changes nothing else. The flag stays disabled; enable it + * with a separate call. An already-unarchived flag is returned unchanged. + */ +export const FeatureFlagsUnarchiveCreateParams = /* @__PURE__ */ zod.object({ + id: zod.number().describe('A unique integer value identifying this feature flag.'), + project_id: zod + .string() + .describe( + "Project ID of the project you're trying to access. To find the ID of the project, make a call to \/api\/projects\/." + ), +}) + /** * Bulk delete feature flags by filter criteria or explicit IDs. * diff --git a/services/mcp/src/lib/instructions.ts b/services/mcp/src/lib/instructions.ts index 0f532dd562e6..2ff6ed716e2e 100644 --- a/services/mcp/src/lib/instructions.ts +++ b/services/mcp/src/lib/instructions.ts @@ -232,6 +232,7 @@ export class ToolDomainExtractor { 'calculate', 'complete', 'copy', + 'disable', 'discard', 'duplicate', 'edit', diff --git a/services/mcp/tests/unit/__snapshots__/tool-schemas/feature-flag-archive.json b/services/mcp/tests/unit/__snapshots__/tool-schemas/feature-flag-archive.json new file mode 100644 index 000000000000..88cc27e940e9 --- /dev/null +++ b/services/mcp/tests/unit/__snapshots__/tool-schemas/feature-flag-archive.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "id": { + "description": "A unique integer value identifying this feature flag.", + "type": "number" + } + }, + "required": ["id"], + "type": "object" +} diff --git a/services/mcp/tests/unit/__snapshots__/tool-schemas/feature-flag-disable.json b/services/mcp/tests/unit/__snapshots__/tool-schemas/feature-flag-disable.json new file mode 100644 index 000000000000..88cc27e940e9 --- /dev/null +++ b/services/mcp/tests/unit/__snapshots__/tool-schemas/feature-flag-disable.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "id": { + "description": "A unique integer value identifying this feature flag.", + "type": "number" + } + }, + "required": ["id"], + "type": "object" +} diff --git a/services/mcp/tests/unit/__snapshots__/tool-schemas/feature-flag-enable.json b/services/mcp/tests/unit/__snapshots__/tool-schemas/feature-flag-enable.json new file mode 100644 index 000000000000..88cc27e940e9 --- /dev/null +++ b/services/mcp/tests/unit/__snapshots__/tool-schemas/feature-flag-enable.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "id": { + "description": "A unique integer value identifying this feature flag.", + "type": "number" + } + }, + "required": ["id"], + "type": "object" +} diff --git a/services/mcp/tests/unit/__snapshots__/tool-schemas/feature-flag-unarchive.json b/services/mcp/tests/unit/__snapshots__/tool-schemas/feature-flag-unarchive.json new file mode 100644 index 000000000000..88cc27e940e9 --- /dev/null +++ b/services/mcp/tests/unit/__snapshots__/tool-schemas/feature-flag-unarchive.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "id": { + "description": "A unique integer value identifying this feature flag.", + "type": "number" + } + }, + "required": ["id"], + "type": "object" +} diff --git a/services/mcp/tests/unit/feature-flag-lifecycle-tools.test.ts b/services/mcp/tests/unit/feature-flag-lifecycle-tools.test.ts new file mode 100644 index 000000000000..ecd724bb8fb5 --- /dev/null +++ b/services/mcp/tests/unit/feature-flag-lifecycle-tools.test.ts @@ -0,0 +1,62 @@ +import { describe, expect, it, vi } from 'vitest' +import { z } from 'zod' + +import { GENERATED_TOOL_MAP } from '@/tools/generated' +import { getToolDefinition } from '@/tools/toolDefinitions' +import type { Context } from '@/tools/types' + +const LIFECYCLE_TOOLS = [ + 'feature-flag-enable', + 'feature-flag-disable', + 'feature-flag-archive', + 'feature-flag-unarchive', +] + +describe('feature flag lifecycle tools', () => { + // The snapshot test would also catch a `filters` param appearing here, but it is + // silenced by `vitest -u`. These assertions are not: the whole point of the lifecycle + // tools is that an agent cannot send targeting through them. + it.each(LIFECYCLE_TOOLS)('%s takes the flag id and nothing else', (name) => { + const tool = GENERATED_TOOL_MAP[name]?.() + expect(tool, `${name} is missing from the generated tool map`).not.toBeUndefined() + + const schema = z.toJSONSchema(tool!.schema, { io: 'input', reused: 'inline' }) as { + properties?: Record + required?: string[] + } + + expect(Object.keys(schema.properties ?? {})).toEqual(['id']) + expect(schema.required).toEqual(['id']) + }) + + it.each(LIFECYCLE_TOOLS)('%s declares the write scope and a reversible, idempotent action', (name) => { + const definition = getToolDefinition(name) + + expect(definition.description).toContain('never accepts or replaces the `filters` object') + expect(definition.required_scopes).toEqual(['feature_flag:write']) + expect(definition.annotations?.readOnlyHint).toBe(false) + expect(definition.annotations?.destructiveHint).toBe(false) + expect(definition.annotations?.idempotentHint).toBe(true) + }) + + // The tool name and the URL it posts to come from separate lines of the same YAML entry, + // so nothing above notices `feature-flag-enable` wired to `/disable/`. An agent asked to + // turn a flag on would turn it off. + it.each(LIFECYCLE_TOOLS)('%s posts to its own endpoint', async (name) => { + const requestMock = vi.fn().mockResolvedValue({ id: 42 }) + const context = { + api: { + request: requestMock, + getProjectBaseUrl: () => 'https://app.posthog.com/project/17', + }, + stateManager: { getProjectId: vi.fn().mockResolvedValue('17') }, + } as unknown as Context + + await GENERATED_TOOL_MAP[name]!().handler(context, { id: 42 }) + + expect(requestMock).toHaveBeenCalledWith({ + method: 'POST', + path: `/api/projects/17/feature_flags/42/${name.replace('feature-flag-', '')}/`, + }) + }) +}) diff --git a/services/mcp/tests/unit/instructions.test.ts b/services/mcp/tests/unit/instructions.test.ts index 96234e3a12ca..41e3c1f29a31 100644 --- a/services/mcp/tests/unit/instructions.test.ts +++ b/services/mcp/tests/unit/instructions.test.ts @@ -157,6 +157,7 @@ describe('buildToolDomainsBlock', () => { 'calculate', 'complete', 'copy', + 'disable', 'discard', 'duplicate', 'edit', From 69add5e251e9ea557882926e6894dbdc65a5f641 Mon Sep 17 00:00:00 2001 From: James Gill Date: Tue, 1 Sep 2026 09:32:26 +0100 Subject: [PATCH 2/8] fix(flags): correct the lifecycle tool descriptions An agent asked to turn an archived flag back on reported that the flag did not exist. The enable description sent it to `feature-flag-get-definition-by-key` or `feature-flag-get-all` to resolve the key. Both hide archived flags, so the 400 that names unarchive as the fix was unreachable. Enable now says a key miss can mean an archived flag, and names the `archived=true` lookup. Archiving an enabled flag turns it off, which stops it evaluating everywhere. Only the disable description asked the agent to report that impact. Archive now asks for it too. Disable asks for it unconditionally, because the old condition (the user did not ask for the flag to be turned off) is almost never true for a tool named disable. `update-feature-flag` pointed at `feature-flag-get-all` for key resolution while the four new tools pointed at the by-key tool. It also claimed the lifecycle tools "cannot lose a concurrent edit". In this repo that phrase means a version precondition and a 409, and these endpoints have neither. What they do have is narrower and now stated: they send no `filters`, so a targeting change made between the read and the write survives. The one legitimate use of `active` and `archived` on the generic tool is now named too. The stale-flags skill told agents to disable through `update-feature-flag` while the rewritten description told them to prefer the dedicated tool. An agent loading both got two answers. `flag-unarchive-stays-off` could pass without unarchiving anything. Its escape clause accepted "the flag does not exist", which is what a plain key lookup returns for an archived flag. The benchmark comment also claimed a flag the project owns. Nothing seeds it, and one shared key makes the run order-dependent. Regenerated the tool-definition JSON from the YAML. Co-Authored-By: Claude Opus 5 (1M context) --- products/feature_flags/mcp/tools.yaml | 31 +++++++++++++------ .../cleaning-up-stale-feature-flags/SKILL.md | 6 ++-- services/mcp/evals/benchmark/tasks.yaml | 16 ++++++---- .../schema/generated-tool-definitions.json | 8 ++--- services/mcp/schema/tool-definitions-all.json | 8 ++--- 5 files changed, 43 insertions(+), 26 deletions(-) diff --git a/products/feature_flags/mcp/tools.yaml b/products/feature_flags/mcp/tools.yaml index fc6ee86f734f..2f8813ae0de8 100644 --- a/products/feature_flags/mcp/tools.yaml +++ b/products/feature_flags/mcp/tools.yaml @@ -114,6 +114,11 @@ tools: and survey history is preserved. There is no request body. + When the flag is enabled, archiving stops it evaluating everywhere. Tell the user what else this + affects: linked experiments, surveys, early access features and session replay settings all appear in the + flag's full definition. + + Returns 400 when the flag is enabled and other active flags depend on it. Returns 409 when an approval policy gates disabling. Archiving an already-archived flag succeeds and changes nothing, so retrying is safe. @@ -144,10 +149,10 @@ tools: so targeting, rollout percentages, variants and payloads cannot change. There is no request body. - A disabled flag stops evaluating everywhere, so every caller falls back to its own default. When the user - did not explicitly ask for the flag to be turned off, read the full definition with - `feature-flag-get-definition` first and tell them what else this affects: linked experiments, surveys, early - access features and session replay settings all appear there. + A disabled flag stops evaluating everywhere, so every caller falls back to its own default. Tell the user + what else this affects: linked experiments, surveys, early access features and session replay settings all + appear in the flag's full definition. `feature-flag-get-definition-by-key` already returns it, and + `feature-flag-get-definition` returns it by ID. Returns 400 when other active flags depend on this one; `feature-flags-dependent-flags-retrieve` lists them. @@ -186,6 +191,11 @@ tools: changes nothing, so retrying is safe. + A key lookup that finds nothing does not mean the flag is absent. Archived flags are hidden from + `feature-flag-get-definition-by-key` and from `feature-flag-get-all`. Pass `archived=true` to + `feature-flag-get-all` before you report the flag as missing. + + To turn the flag on at a future time instead, use `scheduled-changes-create` with the `update_status` operation. param_overrides: @@ -617,14 +627,17 @@ tools: Prefer a dedicated tool whenever the request matches one. `feature-flag-enable`, `feature-flag-disable`, - `feature-flag-archive` and `feature-flag-unarchive` each change one state field, take no `filters`, and - cannot lose a concurrent edit. + `feature-flag-archive` and `feature-flag-unarchive` each change one state field and send no `filters`. A + targeting change someone else made between your read and your write is therefore not overwritten. Send + `active` or `archived` here only when the same request also changes targeting or another field, so the + whole edit lands in one write. Rollout conditions, variants, and payloads live in the structured `filters` param, and sending `filters` - replaces the whole object, so read the flag first with `feature-flag-get-definition` and merge your change. - To apply a change at a future time instead, use `scheduled-changes-create`. Use `feature-flag-get-all` to - find the numeric ID from a key. + replaces the whole object, so read the flag first and merge your change. Use + `feature-flag-get-definition-by-key` when you have the string key: it returns the numeric ID and the full + definition in one call. Use `feature-flag-get-definition` when you already have the ID. To apply a change at + a future time instead, use `scheduled-changes-create`. include_params: - key - name diff --git a/products/feature_flags/skills/cleaning-up-stale-feature-flags/SKILL.md b/products/feature_flags/skills/cleaning-up-stale-feature-flags/SKILL.md index 7947c716c4eb..521d95363ab5 100644 --- a/products/feature_flags/skills/cleaning-up-stale-feature-flags/SKILL.md +++ b/products/feature_flags/skills/cleaning-up-stale-feature-flags/SKILL.md @@ -113,7 +113,7 @@ Present the full cleanup prompt in a copyable format so the user can paste it di Present the user with both options and their tradeoffs: -- **Disable** (`active: false`) via `posthog:update-feature-flag`: The flag stops being evaluated but the configuration is preserved. If something was missed in the code cleanup, re-enabling is instant. Recommended as the default. +- **Disable** via `posthog:feature-flag-disable`: The flag stops being evaluated but the configuration is preserved. The tool takes only the flag id, so targeting cannot change. If something was missed in the code cleanup, re-enabling is instant. Recommended as the default. - **Delete** via `posthog:delete-feature-flag`: A soft-delete — the flag is marked as deleted but not physically removed. Keeps the flag list clean, but re-enabling requires recreating the flag. Better for flags the user is confident they'll never need again. Once the user has chosen and confirms their code changes are deployed, apply the chosen action one flag at a time. Confirm each action so it's easy to stop if something goes wrong. @@ -171,7 +171,7 @@ Agent steps: - Delete: removes from the list, but you'd need to recreate if needed" - User confirms: "Disable them, code is deployed" -- Disable each flag using posthog:update-feature-flag (active: false) +- Disable each flag using posthog:feature-flag-disable - Confirm: "Both flags are now disabled in PostHog." ``` @@ -190,5 +190,5 @@ Agent steps: - `posthog:feature-flag-get-all`: List and search feature flags (supports `active: "STALE"` filter) - `posthog:feature-flag-get-definition`: Get full flag details including experiment associations - `posthog:feature-flags-status-retrieve`: Get the status and reason for a single flag -- `posthog:update-feature-flag`: Disable a flag by setting `active: false` +- `posthog:feature-flag-disable`: Turn a flag off without touching its targeting - `posthog:delete-feature-flag`: Soft-delete a flag diff --git a/services/mcp/evals/benchmark/tasks.yaml b/services/mcp/evals/benchmark/tasks.yaml index 903af3a88ecb..551b7ec539d7 100644 --- a/services/mcp/evals/benchmark/tasks.yaml +++ b/services/mcp/evals/benchmark/tasks.yaml @@ -212,11 +212,14 @@ tasks: acceptable_tools: [feature-flag-get-all, feature-flag-get-definition] success_criteria: "Resolves the flag by its key ('mcp-analytics') directly — does not guess a numeric id." - # The lifecycle tasks below act on a flag the benchmark project owns, so an agent-mode - # run flips only that flag's state. They score tool selection whether or not - # `mcp-eval-lifecycle` exists, because a missing flag must produce a clear answer rather - # than a fallback to the generic update tool. Seed it (enabled, not archived) to also - # exercise the state change itself. + # The lifecycle tasks below all act on one flag key, so an agent-mode run flips only that + # flag's state. They score tool selection whether or not `mcp-eval-lifecycle` exists, + # because a missing flag must produce a clear answer rather than a fallback to the generic + # update tool. Nothing seeds the key today; seed it to also exercise the state change. + # One shared key makes the run order-dependent. `flag-unarchive-stays-off` needs the flag + # archived, and only `flag-archive-completed` leaves it that way. A second full run also + # starts from the targeting the first one wrote. Give each task its own key before relying + # on the state-change half of these scores. - id: flag-enable-by-key category: feature-flags intent: 'Turn on the mcp-eval-lifecycle feature flag.' @@ -258,7 +261,8 @@ tasks: acceptable_tools: [feature-flag-get-all, feature-flag-get-definition-by-key, feature-flag-get-definition] success_criteria: > Calls feature-flag-unarchive with no filters payload and does not enable it, saying the flag is still turned - off. If the flag does not exist, says so rather than reaching for update-feature-flag. + off. Finds the archived flag by passing archived=true to feature-flag-get-all; a plain key lookup returning + nothing is not grounds for reporting that the flag does not exist. - id: flag-targeting-edit-uses-update category: feature-flags diff --git a/services/mcp/schema/generated-tool-definitions.json b/services/mcp/schema/generated-tool-definitions.json index 35faefa1d6bc..6e5a7cb88491 100644 --- a/services/mcp/schema/generated-tool-definitions.json +++ b/services/mcp/schema/generated-tool-definitions.json @@ -4822,7 +4822,7 @@ } }, "feature-flag-archive": { - "description": "Archive a feature flag, hiding it from the default flag list. Requires the flag's numeric ID; use `feature-flag-get-definition-by-key` to get it from a key, or `feature-flag-get-all` to search by key or name.\n\nSets `archived` to true. An archived flag must be disabled, so an enabled flag is turned off in the same call. Nothing else changes: this tool never accepts or replaces the `filters` object, and linked experiment and survey history is preserved. There is no request body.\n\nReturns 400 when the flag is enabled and other active flags depend on it. Returns 409 when an approval policy gates disabling. Archiving an already-archived flag succeeds and changes nothing, so retrying is safe.\n\nUse `feature-flag-unarchive` to put the flag back in the list, or `delete-feature-flag` when the user wants it gone rather than tidied away.", + "description": "Archive a feature flag, hiding it from the default flag list. Requires the flag's numeric ID; use `feature-flag-get-definition-by-key` to get it from a key, or `feature-flag-get-all` to search by key or name.\n\nSets `archived` to true. An archived flag must be disabled, so an enabled flag is turned off in the same call. Nothing else changes: this tool never accepts or replaces the `filters` object, and linked experiment and survey history is preserved. There is no request body.\n\nWhen the flag is enabled, archiving stops it evaluating everywhere. Tell the user what else this affects: linked experiments, surveys, early access features and session replay settings all appear in the flag's full definition.\n\nReturns 400 when the flag is enabled and other active flags depend on it. Returns 409 when an approval policy gates disabling. Archiving an already-archived flag succeeds and changes nothing, so retrying is safe.\n\nUse `feature-flag-unarchive` to put the flag back in the list, or `delete-feature-flag` when the user wants it gone rather than tidied away.", "category": "Feature flags", "feature": "flags", "summary": "Archive feature flag", @@ -4836,7 +4836,7 @@ } }, "feature-flag-disable": { - "description": "Turn a feature flag off. Requires the flag's numeric ID; use `feature-flag-get-definition-by-key` to get it from a key, or `feature-flag-get-all` to search by key or name.\n\nSets `active` to false and changes nothing else. This tool never accepts or replaces the `filters` object, so targeting, rollout percentages, variants and payloads cannot change. There is no request body.\n\nA disabled flag stops evaluating everywhere, so every caller falls back to its own default. When the user did not explicitly ask for the flag to be turned off, read the full definition with `feature-flag-get-definition` first and tell them what else this affects: linked experiments, surveys, early access features and session replay settings all appear there.\n\nReturns 400 when other active flags depend on this one; `feature-flags-dependent-flags-retrieve` lists them. Returns 409 when an approval policy gates disabling. Disabling an already-disabled flag succeeds and changes nothing, so retrying is safe.\n\nTo turn the flag off at a future time instead, use `scheduled-changes-create` with the `update_status` operation.", + "description": "Turn a feature flag off. Requires the flag's numeric ID; use `feature-flag-get-definition-by-key` to get it from a key, or `feature-flag-get-all` to search by key or name.\n\nSets `active` to false and changes nothing else. This tool never accepts or replaces the `filters` object, so targeting, rollout percentages, variants and payloads cannot change. There is no request body.\n\nA disabled flag stops evaluating everywhere, so every caller falls back to its own default. Tell the user what else this affects: linked experiments, surveys, early access features and session replay settings all appear in the flag's full definition. `feature-flag-get-definition-by-key` already returns it, and `feature-flag-get-definition` returns it by ID.\n\nReturns 400 when other active flags depend on this one; `feature-flags-dependent-flags-retrieve` lists them. Returns 409 when an approval policy gates disabling. Disabling an already-disabled flag succeeds and changes nothing, so retrying is safe.\n\nTo turn the flag off at a future time instead, use `scheduled-changes-create` with the `update_status` operation.", "category": "Feature flags", "feature": "flags", "summary": "Disable feature flag", @@ -4850,7 +4850,7 @@ } }, "feature-flag-enable": { - "description": "Turn a feature flag on. Requires the flag's numeric ID; use `feature-flag-get-definition-by-key` to get it from a key, or `feature-flag-get-all` to search by key or name.\n\nSets `active` to true and changes nothing else. This tool never accepts or replaces the `filters` object, so targeting, rollout percentages, variants and payloads cannot change. There is no request body. To change who the flag is served to, use `update-feature-flag`.\n\nReturns 400 when the flag is archived (call `feature-flag-unarchive` first) or when a flag it depends on is disabled. Returns 409 when an approval policy gates enabling. Enabling an already-enabled flag succeeds and changes nothing, so retrying is safe.\n\nTo turn the flag on at a future time instead, use `scheduled-changes-create` with the `update_status` operation.", + "description": "Turn a feature flag on. Requires the flag's numeric ID; use `feature-flag-get-definition-by-key` to get it from a key, or `feature-flag-get-all` to search by key or name.\n\nSets `active` to true and changes nothing else. This tool never accepts or replaces the `filters` object, so targeting, rollout percentages, variants and payloads cannot change. There is no request body. To change who the flag is served to, use `update-feature-flag`.\n\nReturns 400 when the flag is archived (call `feature-flag-unarchive` first) or when a flag it depends on is disabled. Returns 409 when an approval policy gates enabling. Enabling an already-enabled flag succeeds and changes nothing, so retrying is safe.\n\nA key lookup that finds nothing does not mean the flag is absent. Archived flags are hidden from `feature-flag-get-definition-by-key` and from `feature-flag-get-all`. Pass `archived=true` to `feature-flag-get-all` before you report the flag as missing.\n\nTo turn the flag on at a future time instead, use `scheduled-changes-create` with the `update_status` operation.", "category": "Feature flags", "feature": "flags", "summary": "Enable feature flag", @@ -10838,7 +10838,7 @@ "feature_flag": "tasks" }, "update-feature-flag": { - "description": "Update a feature flag by numeric ID (partial update: only the fields you include are changed). Use this for edits that have no dedicated tool, such as release conditions, rollout percentages, variants, payloads, tags and the description.\n\nPrefer a dedicated tool whenever the request matches one. `feature-flag-enable`, `feature-flag-disable`, `feature-flag-archive` and `feature-flag-unarchive` each change one state field, take no `filters`, and cannot lose a concurrent edit.\n\nRollout conditions, variants, and payloads live in the structured `filters` param, and sending `filters` replaces the whole object, so read the flag first with `feature-flag-get-definition` and merge your change. To apply a change at a future time instead, use `scheduled-changes-create`. Use `feature-flag-get-all` to find the numeric ID from a key.", + "description": "Update a feature flag by numeric ID (partial update: only the fields you include are changed). Use this for edits that have no dedicated tool, such as release conditions, rollout percentages, variants, payloads, tags and the description.\n\nPrefer a dedicated tool whenever the request matches one. `feature-flag-enable`, `feature-flag-disable`, `feature-flag-archive` and `feature-flag-unarchive` each change one state field and send no `filters`. A targeting change someone else made between your read and your write is therefore not overwritten. Send `active` or `archived` here only when the same request also changes targeting or another field, so the whole edit lands in one write.\n\nRollout conditions, variants, and payloads live in the structured `filters` param, and sending `filters` replaces the whole object, so read the flag first and merge your change. Use `feature-flag-get-definition-by-key` when you have the string key: it returns the numeric ID and the full definition in one call. Use `feature-flag-get-definition` when you already have the ID. To apply a change at a future time instead, use `scheduled-changes-create`.", "category": "Feature flags", "feature": "flags", "summary": "Update feature flag", diff --git a/services/mcp/schema/tool-definitions-all.json b/services/mcp/schema/tool-definitions-all.json index 01b77f3abdaa..4c0e78bd65de 100644 --- a/services/mcp/schema/tool-definitions-all.json +++ b/services/mcp/schema/tool-definitions-all.json @@ -4977,7 +4977,7 @@ } }, "feature-flag-archive": { - "description": "Archive a feature flag, hiding it from the default flag list. Requires the flag's numeric ID; use `feature-flag-get-definition-by-key` to get it from a key, or `feature-flag-get-all` to search by key or name.\n\nSets `archived` to true. An archived flag must be disabled, so an enabled flag is turned off in the same call. Nothing else changes: this tool never accepts or replaces the `filters` object, and linked experiment and survey history is preserved. There is no request body.\n\nReturns 400 when the flag is enabled and other active flags depend on it. Returns 409 when an approval policy gates disabling. Archiving an already-archived flag succeeds and changes nothing, so retrying is safe.\n\nUse `feature-flag-unarchive` to put the flag back in the list, or `delete-feature-flag` when the user wants it gone rather than tidied away.", + "description": "Archive a feature flag, hiding it from the default flag list. Requires the flag's numeric ID; use `feature-flag-get-definition-by-key` to get it from a key, or `feature-flag-get-all` to search by key or name.\n\nSets `archived` to true. An archived flag must be disabled, so an enabled flag is turned off in the same call. Nothing else changes: this tool never accepts or replaces the `filters` object, and linked experiment and survey history is preserved. There is no request body.\n\nWhen the flag is enabled, archiving stops it evaluating everywhere. Tell the user what else this affects: linked experiments, surveys, early access features and session replay settings all appear in the flag's full definition.\n\nReturns 400 when the flag is enabled and other active flags depend on it. Returns 409 when an approval policy gates disabling. Archiving an already-archived flag succeeds and changes nothing, so retrying is safe.\n\nUse `feature-flag-unarchive` to put the flag back in the list, or `delete-feature-flag` when the user wants it gone rather than tidied away.", "category": "Feature flags", "feature": "flags", "summary": "Archive feature flag", @@ -4991,7 +4991,7 @@ } }, "feature-flag-disable": { - "description": "Turn a feature flag off. Requires the flag's numeric ID; use `feature-flag-get-definition-by-key` to get it from a key, or `feature-flag-get-all` to search by key or name.\n\nSets `active` to false and changes nothing else. This tool never accepts or replaces the `filters` object, so targeting, rollout percentages, variants and payloads cannot change. There is no request body.\n\nA disabled flag stops evaluating everywhere, so every caller falls back to its own default. When the user did not explicitly ask for the flag to be turned off, read the full definition with `feature-flag-get-definition` first and tell them what else this affects: linked experiments, surveys, early access features and session replay settings all appear there.\n\nReturns 400 when other active flags depend on this one; `feature-flags-dependent-flags-retrieve` lists them. Returns 409 when an approval policy gates disabling. Disabling an already-disabled flag succeeds and changes nothing, so retrying is safe.\n\nTo turn the flag off at a future time instead, use `scheduled-changes-create` with the `update_status` operation.", + "description": "Turn a feature flag off. Requires the flag's numeric ID; use `feature-flag-get-definition-by-key` to get it from a key, or `feature-flag-get-all` to search by key or name.\n\nSets `active` to false and changes nothing else. This tool never accepts or replaces the `filters` object, so targeting, rollout percentages, variants and payloads cannot change. There is no request body.\n\nA disabled flag stops evaluating everywhere, so every caller falls back to its own default. Tell the user what else this affects: linked experiments, surveys, early access features and session replay settings all appear in the flag's full definition. `feature-flag-get-definition-by-key` already returns it, and `feature-flag-get-definition` returns it by ID.\n\nReturns 400 when other active flags depend on this one; `feature-flags-dependent-flags-retrieve` lists them. Returns 409 when an approval policy gates disabling. Disabling an already-disabled flag succeeds and changes nothing, so retrying is safe.\n\nTo turn the flag off at a future time instead, use `scheduled-changes-create` with the `update_status` operation.", "category": "Feature flags", "feature": "flags", "summary": "Disable feature flag", @@ -5005,7 +5005,7 @@ } }, "feature-flag-enable": { - "description": "Turn a feature flag on. Requires the flag's numeric ID; use `feature-flag-get-definition-by-key` to get it from a key, or `feature-flag-get-all` to search by key or name.\n\nSets `active` to true and changes nothing else. This tool never accepts or replaces the `filters` object, so targeting, rollout percentages, variants and payloads cannot change. There is no request body. To change who the flag is served to, use `update-feature-flag`.\n\nReturns 400 when the flag is archived (call `feature-flag-unarchive` first) or when a flag it depends on is disabled. Returns 409 when an approval policy gates enabling. Enabling an already-enabled flag succeeds and changes nothing, so retrying is safe.\n\nTo turn the flag on at a future time instead, use `scheduled-changes-create` with the `update_status` operation.", + "description": "Turn a feature flag on. Requires the flag's numeric ID; use `feature-flag-get-definition-by-key` to get it from a key, or `feature-flag-get-all` to search by key or name.\n\nSets `active` to true and changes nothing else. This tool never accepts or replaces the `filters` object, so targeting, rollout percentages, variants and payloads cannot change. There is no request body. To change who the flag is served to, use `update-feature-flag`.\n\nReturns 400 when the flag is archived (call `feature-flag-unarchive` first) or when a flag it depends on is disabled. Returns 409 when an approval policy gates enabling. Enabling an already-enabled flag succeeds and changes nothing, so retrying is safe.\n\nA key lookup that finds nothing does not mean the flag is absent. Archived flags are hidden from `feature-flag-get-definition-by-key` and from `feature-flag-get-all`. Pass `archived=true` to `feature-flag-get-all` before you report the flag as missing.\n\nTo turn the flag on at a future time instead, use `scheduled-changes-create` with the `update_status` operation.", "category": "Feature flags", "feature": "flags", "summary": "Enable feature flag", @@ -11466,7 +11466,7 @@ "feature_flag": "tasks" }, "update-feature-flag": { - "description": "Update a feature flag by numeric ID (partial update: only the fields you include are changed). Use this for edits that have no dedicated tool, such as release conditions, rollout percentages, variants, payloads, tags and the description.\n\nPrefer a dedicated tool whenever the request matches one. `feature-flag-enable`, `feature-flag-disable`, `feature-flag-archive` and `feature-flag-unarchive` each change one state field, take no `filters`, and cannot lose a concurrent edit.\n\nRollout conditions, variants, and payloads live in the structured `filters` param, and sending `filters` replaces the whole object, so read the flag first with `feature-flag-get-definition` and merge your change. To apply a change at a future time instead, use `scheduled-changes-create`. Use `feature-flag-get-all` to find the numeric ID from a key.", + "description": "Update a feature flag by numeric ID (partial update: only the fields you include are changed). Use this for edits that have no dedicated tool, such as release conditions, rollout percentages, variants, payloads, tags and the description.\n\nPrefer a dedicated tool whenever the request matches one. `feature-flag-enable`, `feature-flag-disable`, `feature-flag-archive` and `feature-flag-unarchive` each change one state field and send no `filters`. A targeting change someone else made between your read and your write is therefore not overwritten. Send `active` or `archived` here only when the same request also changes targeting or another field, so the whole edit lands in one write.\n\nRollout conditions, variants, and payloads live in the structured `filters` param, and sending `filters` replaces the whole object, so read the flag first and merge your change. Use `feature-flag-get-definition-by-key` when you have the string key: it returns the numeric ID and the full definition in one call. Use `feature-flag-get-definition` when you already have the ID. To apply a change at a future time instead, use `scheduled-changes-create`.", "category": "Feature flags", "feature": "flags", "summary": "Update feature flag", From b0cac9c1cb122ff08222ed77ff3b63aa77754e32 Mon Sep 17 00:00:00 2001 From: "tests-posthog[bot]" <250237707+tests-posthog[bot]@users.noreply.github.com> Date: Tue, 1 Sep 2026 08:52:24 +0000 Subject: [PATCH 3/8] chore: update OpenAPI generated types --- products/feature_flags/mcp/tools.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/products/feature_flags/mcp/tools.yaml b/products/feature_flags/mcp/tools.yaml index 2f8813ae0de8..5b3226ed6415 100644 --- a/products/feature_flags/mcp/tools.yaml +++ b/products/feature_flags/mcp/tools.yaml @@ -114,9 +114,9 @@ tools: and survey history is preserved. There is no request body. - When the flag is enabled, archiving stops it evaluating everywhere. Tell the user what else this - affects: linked experiments, surveys, early access features and session replay settings all appear in the - flag's full definition. + When the flag is enabled, archiving stops it evaluating everywhere. Tell the user what else this affects: + linked experiments, surveys, early access features and session replay settings all appear in the flag's full + definition. Returns 400 when the flag is enabled and other active flags depend on it. Returns 409 when an approval @@ -629,8 +629,8 @@ tools: Prefer a dedicated tool whenever the request matches one. `feature-flag-enable`, `feature-flag-disable`, `feature-flag-archive` and `feature-flag-unarchive` each change one state field and send no `filters`. A targeting change someone else made between your read and your write is therefore not overwritten. Send - `active` or `archived` here only when the same request also changes targeting or another field, so the - whole edit lands in one write. + `active` or `archived` here only when the same request also changes targeting or another field, so the whole + edit lands in one write. Rollout conditions, variants, and payloads live in the structured `filters` param, and sending `filters` From b8eb2a68d9a1e4ce9c9851309aacd3f33507ea1d Mon Sep 17 00:00:00 2001 From: "posthog[bot]" <206114724+posthog[bot]@users.noreply.github.com> Date: Tue, 1 Sep 2026 09:26:17 +0000 Subject: [PATCH 4/8] fix(flags): warn archive and disable tools about hidden archived flags The archive and disable tool descriptions promise a safe no-op when the flag is already in the target state, but route key resolution through `feature-flag-get-definition-by-key`, which hides archived flags. A retry by key after a flag is archived (or a disable of an already-archived flag) missed and reported the flag as absent. Add the same archived-lookup fallback note the sibling `feature-flag-enable` and `feature-flag-unarchive` tools already carry: pass `archived=true` to `feature-flag-get-all` before reporting the flag missing. Regenerate the two derived tool-definition JSON files to match. Generated-By: PostHog Desktop Task-Id: 78f746f0-750e-41db-86f0-a9750a91318f --- products/feature_flags/mcp/tools.yaml | 11 +++++++++++ services/mcp/schema/generated-tool-definitions.json | 4 ++-- services/mcp/schema/tool-definitions-all.json | 4 ++-- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/products/feature_flags/mcp/tools.yaml b/products/feature_flags/mcp/tools.yaml index 5b3226ed6415..48867429abf3 100644 --- a/products/feature_flags/mcp/tools.yaml +++ b/products/feature_flags/mcp/tools.yaml @@ -124,6 +124,12 @@ tools: safe. + A key lookup that finds nothing does not mean the flag is absent. Archived flags are hidden from + `feature-flag-get-definition-by-key` and from `feature-flag-get-all`, so a flag that is already archived + looks missing on a retry. Pass `archived=true` to `feature-flag-get-all` before you report the flag as + missing. + + Use `feature-flag-unarchive` to put the flag back in the list, or `delete-feature-flag` when the user wants it gone rather than tidied away. param_overrides: @@ -160,6 +166,11 @@ tools: nothing, so retrying is safe. + A key lookup that finds nothing does not mean the flag is absent. Archived flags are hidden from + `feature-flag-get-definition-by-key` and from `feature-flag-get-all`, and an archived flag is already + disabled. Pass `archived=true` to `feature-flag-get-all` before you report the flag as missing. + + To turn the flag off at a future time instead, use `scheduled-changes-create` with the `update_status` operation. param_overrides: diff --git a/services/mcp/schema/generated-tool-definitions.json b/services/mcp/schema/generated-tool-definitions.json index 6e5a7cb88491..2bac8d0efb85 100644 --- a/services/mcp/schema/generated-tool-definitions.json +++ b/services/mcp/schema/generated-tool-definitions.json @@ -4822,7 +4822,7 @@ } }, "feature-flag-archive": { - "description": "Archive a feature flag, hiding it from the default flag list. Requires the flag's numeric ID; use `feature-flag-get-definition-by-key` to get it from a key, or `feature-flag-get-all` to search by key or name.\n\nSets `archived` to true. An archived flag must be disabled, so an enabled flag is turned off in the same call. Nothing else changes: this tool never accepts or replaces the `filters` object, and linked experiment and survey history is preserved. There is no request body.\n\nWhen the flag is enabled, archiving stops it evaluating everywhere. Tell the user what else this affects: linked experiments, surveys, early access features and session replay settings all appear in the flag's full definition.\n\nReturns 400 when the flag is enabled and other active flags depend on it. Returns 409 when an approval policy gates disabling. Archiving an already-archived flag succeeds and changes nothing, so retrying is safe.\n\nUse `feature-flag-unarchive` to put the flag back in the list, or `delete-feature-flag` when the user wants it gone rather than tidied away.", + "description": "Archive a feature flag, hiding it from the default flag list. Requires the flag's numeric ID; use `feature-flag-get-definition-by-key` to get it from a key, or `feature-flag-get-all` to search by key or name.\n\nSets `archived` to true. An archived flag must be disabled, so an enabled flag is turned off in the same call. Nothing else changes: this tool never accepts or replaces the `filters` object, and linked experiment and survey history is preserved. There is no request body.\n\nWhen the flag is enabled, archiving stops it evaluating everywhere. Tell the user what else this affects: linked experiments, surveys, early access features and session replay settings all appear in the flag's full definition.\n\nReturns 400 when the flag is enabled and other active flags depend on it. Returns 409 when an approval policy gates disabling. Archiving an already-archived flag succeeds and changes nothing, so retrying is safe.\n\nA key lookup that finds nothing does not mean the flag is absent. Archived flags are hidden from `feature-flag-get-definition-by-key` and from `feature-flag-get-all`, so a flag that is already archived looks missing on a retry. Pass `archived=true` to `feature-flag-get-all` before you report the flag as missing.\n\nUse `feature-flag-unarchive` to put the flag back in the list, or `delete-feature-flag` when the user wants it gone rather than tidied away.", "category": "Feature flags", "feature": "flags", "summary": "Archive feature flag", @@ -4836,7 +4836,7 @@ } }, "feature-flag-disable": { - "description": "Turn a feature flag off. Requires the flag's numeric ID; use `feature-flag-get-definition-by-key` to get it from a key, or `feature-flag-get-all` to search by key or name.\n\nSets `active` to false and changes nothing else. This tool never accepts or replaces the `filters` object, so targeting, rollout percentages, variants and payloads cannot change. There is no request body.\n\nA disabled flag stops evaluating everywhere, so every caller falls back to its own default. Tell the user what else this affects: linked experiments, surveys, early access features and session replay settings all appear in the flag's full definition. `feature-flag-get-definition-by-key` already returns it, and `feature-flag-get-definition` returns it by ID.\n\nReturns 400 when other active flags depend on this one; `feature-flags-dependent-flags-retrieve` lists them. Returns 409 when an approval policy gates disabling. Disabling an already-disabled flag succeeds and changes nothing, so retrying is safe.\n\nTo turn the flag off at a future time instead, use `scheduled-changes-create` with the `update_status` operation.", + "description": "Turn a feature flag off. Requires the flag's numeric ID; use `feature-flag-get-definition-by-key` to get it from a key, or `feature-flag-get-all` to search by key or name.\n\nSets `active` to false and changes nothing else. This tool never accepts or replaces the `filters` object, so targeting, rollout percentages, variants and payloads cannot change. There is no request body.\n\nA disabled flag stops evaluating everywhere, so every caller falls back to its own default. Tell the user what else this affects: linked experiments, surveys, early access features and session replay settings all appear in the flag's full definition. `feature-flag-get-definition-by-key` already returns it, and `feature-flag-get-definition` returns it by ID.\n\nReturns 400 when other active flags depend on this one; `feature-flags-dependent-flags-retrieve` lists them. Returns 409 when an approval policy gates disabling. Disabling an already-disabled flag succeeds and changes nothing, so retrying is safe.\n\nA key lookup that finds nothing does not mean the flag is absent. Archived flags are hidden from `feature-flag-get-definition-by-key` and from `feature-flag-get-all`, and an archived flag is already disabled. Pass `archived=true` to `feature-flag-get-all` before you report the flag as missing.\n\nTo turn the flag off at a future time instead, use `scheduled-changes-create` with the `update_status` operation.", "category": "Feature flags", "feature": "flags", "summary": "Disable feature flag", diff --git a/services/mcp/schema/tool-definitions-all.json b/services/mcp/schema/tool-definitions-all.json index 4c0e78bd65de..b5fdeb56829e 100644 --- a/services/mcp/schema/tool-definitions-all.json +++ b/services/mcp/schema/tool-definitions-all.json @@ -4977,7 +4977,7 @@ } }, "feature-flag-archive": { - "description": "Archive a feature flag, hiding it from the default flag list. Requires the flag's numeric ID; use `feature-flag-get-definition-by-key` to get it from a key, or `feature-flag-get-all` to search by key or name.\n\nSets `archived` to true. An archived flag must be disabled, so an enabled flag is turned off in the same call. Nothing else changes: this tool never accepts or replaces the `filters` object, and linked experiment and survey history is preserved. There is no request body.\n\nWhen the flag is enabled, archiving stops it evaluating everywhere. Tell the user what else this affects: linked experiments, surveys, early access features and session replay settings all appear in the flag's full definition.\n\nReturns 400 when the flag is enabled and other active flags depend on it. Returns 409 when an approval policy gates disabling. Archiving an already-archived flag succeeds and changes nothing, so retrying is safe.\n\nUse `feature-flag-unarchive` to put the flag back in the list, or `delete-feature-flag` when the user wants it gone rather than tidied away.", + "description": "Archive a feature flag, hiding it from the default flag list. Requires the flag's numeric ID; use `feature-flag-get-definition-by-key` to get it from a key, or `feature-flag-get-all` to search by key or name.\n\nSets `archived` to true. An archived flag must be disabled, so an enabled flag is turned off in the same call. Nothing else changes: this tool never accepts or replaces the `filters` object, and linked experiment and survey history is preserved. There is no request body.\n\nWhen the flag is enabled, archiving stops it evaluating everywhere. Tell the user what else this affects: linked experiments, surveys, early access features and session replay settings all appear in the flag's full definition.\n\nReturns 400 when the flag is enabled and other active flags depend on it. Returns 409 when an approval policy gates disabling. Archiving an already-archived flag succeeds and changes nothing, so retrying is safe.\n\nA key lookup that finds nothing does not mean the flag is absent. Archived flags are hidden from `feature-flag-get-definition-by-key` and from `feature-flag-get-all`, so a flag that is already archived looks missing on a retry. Pass `archived=true` to `feature-flag-get-all` before you report the flag as missing.\n\nUse `feature-flag-unarchive` to put the flag back in the list, or `delete-feature-flag` when the user wants it gone rather than tidied away.", "category": "Feature flags", "feature": "flags", "summary": "Archive feature flag", @@ -4991,7 +4991,7 @@ } }, "feature-flag-disable": { - "description": "Turn a feature flag off. Requires the flag's numeric ID; use `feature-flag-get-definition-by-key` to get it from a key, or `feature-flag-get-all` to search by key or name.\n\nSets `active` to false and changes nothing else. This tool never accepts or replaces the `filters` object, so targeting, rollout percentages, variants and payloads cannot change. There is no request body.\n\nA disabled flag stops evaluating everywhere, so every caller falls back to its own default. Tell the user what else this affects: linked experiments, surveys, early access features and session replay settings all appear in the flag's full definition. `feature-flag-get-definition-by-key` already returns it, and `feature-flag-get-definition` returns it by ID.\n\nReturns 400 when other active flags depend on this one; `feature-flags-dependent-flags-retrieve` lists them. Returns 409 when an approval policy gates disabling. Disabling an already-disabled flag succeeds and changes nothing, so retrying is safe.\n\nTo turn the flag off at a future time instead, use `scheduled-changes-create` with the `update_status` operation.", + "description": "Turn a feature flag off. Requires the flag's numeric ID; use `feature-flag-get-definition-by-key` to get it from a key, or `feature-flag-get-all` to search by key or name.\n\nSets `active` to false and changes nothing else. This tool never accepts or replaces the `filters` object, so targeting, rollout percentages, variants and payloads cannot change. There is no request body.\n\nA disabled flag stops evaluating everywhere, so every caller falls back to its own default. Tell the user what else this affects: linked experiments, surveys, early access features and session replay settings all appear in the flag's full definition. `feature-flag-get-definition-by-key` already returns it, and `feature-flag-get-definition` returns it by ID.\n\nReturns 400 when other active flags depend on this one; `feature-flags-dependent-flags-retrieve` lists them. Returns 409 when an approval policy gates disabling. Disabling an already-disabled flag succeeds and changes nothing, so retrying is safe.\n\nA key lookup that finds nothing does not mean the flag is absent. Archived flags are hidden from `feature-flag-get-definition-by-key` and from `feature-flag-get-all`, and an archived flag is already disabled. Pass `archived=true` to `feature-flag-get-all` before you report the flag as missing.\n\nTo turn the flag off at a future time instead, use `scheduled-changes-create` with the `update_status` operation.", "category": "Feature flags", "feature": "flags", "summary": "Disable feature flag", From e5508c76760e369d70f16f41efa006b8e87d4feb Mon Sep 17 00:00:00 2001 From: "posthog[bot]" <206114724+posthog[bot]@users.noreply.github.com> Date: Tue, 1 Sep 2026 09:30:13 +0000 Subject: [PATCH 5/8] fix(flags): correct archived-lookup examples in lifecycle tool copy The lifecycle tool descriptions told agents to `Pass archived=true to feature-flag-get-all`, but the list endpoint's `archived` param is a string enum (`"false"` / `"true"`), so a JSON boolean fails schema validation and costs the agent a turn before it can find the archived flag. Show the exact call argument instead: `{"archived":"true"}`. Applies to the enable, disable, archive and unarchive descriptions, and regenerates the two derived tool-definition JSON files. Only the feature-flags copy changes; the survey and AI observability tools use real booleans and are untouched. Generated-By: PostHog Desktop Task-Id: 78f746f0-750e-41db-86f0-a9750a91318f --- products/feature_flags/mcp/tools.yaml | 8 ++++---- services/mcp/schema/generated-tool-definitions.json | 8 ++++---- services/mcp/schema/tool-definitions-all.json | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/products/feature_flags/mcp/tools.yaml b/products/feature_flags/mcp/tools.yaml index 48867429abf3..95dae7e41679 100644 --- a/products/feature_flags/mcp/tools.yaml +++ b/products/feature_flags/mcp/tools.yaml @@ -126,7 +126,7 @@ tools: A key lookup that finds nothing does not mean the flag is absent. Archived flags are hidden from `feature-flag-get-definition-by-key` and from `feature-flag-get-all`, so a flag that is already archived - looks missing on a retry. Pass `archived=true` to `feature-flag-get-all` before you report the flag as + looks missing on a retry. Pass `{"archived":"true"}` to `feature-flag-get-all` before you report the flag as missing. @@ -168,7 +168,7 @@ tools: A key lookup that finds nothing does not mean the flag is absent. Archived flags are hidden from `feature-flag-get-definition-by-key` and from `feature-flag-get-all`, and an archived flag is already - disabled. Pass `archived=true` to `feature-flag-get-all` before you report the flag as missing. + disabled. Pass `{"archived":"true"}` to `feature-flag-get-all` before you report the flag as missing. To turn the flag off at a future time instead, use `scheduled-changes-create` with the `update_status` @@ -203,7 +203,7 @@ tools: A key lookup that finds nothing does not mean the flag is absent. Archived flags are hidden from - `feature-flag-get-definition-by-key` and from `feature-flag-get-all`. Pass `archived=true` to + `feature-flag-get-definition-by-key` and from `feature-flag-get-all`. Pass `{"archived":"true"}` to `feature-flag-get-all` before you report the flag as missing. @@ -275,7 +275,7 @@ tools: title: Unarchive feature flag description: > Restore an archived feature flag to the default flag list. Requires the flag's numeric ID; pass - `archived=true` to `feature-flag-get-all` to find archived flags. + `{"archived":"true"}` to `feature-flag-get-all` to find archived flags. Sets `archived` to false and changes nothing else. This tool never accepts or replaces the `filters` object. diff --git a/services/mcp/schema/generated-tool-definitions.json b/services/mcp/schema/generated-tool-definitions.json index 2bac8d0efb85..55805302c1eb 100644 --- a/services/mcp/schema/generated-tool-definitions.json +++ b/services/mcp/schema/generated-tool-definitions.json @@ -4822,7 +4822,7 @@ } }, "feature-flag-archive": { - "description": "Archive a feature flag, hiding it from the default flag list. Requires the flag's numeric ID; use `feature-flag-get-definition-by-key` to get it from a key, or `feature-flag-get-all` to search by key or name.\n\nSets `archived` to true. An archived flag must be disabled, so an enabled flag is turned off in the same call. Nothing else changes: this tool never accepts or replaces the `filters` object, and linked experiment and survey history is preserved. There is no request body.\n\nWhen the flag is enabled, archiving stops it evaluating everywhere. Tell the user what else this affects: linked experiments, surveys, early access features and session replay settings all appear in the flag's full definition.\n\nReturns 400 when the flag is enabled and other active flags depend on it. Returns 409 when an approval policy gates disabling. Archiving an already-archived flag succeeds and changes nothing, so retrying is safe.\n\nA key lookup that finds nothing does not mean the flag is absent. Archived flags are hidden from `feature-flag-get-definition-by-key` and from `feature-flag-get-all`, so a flag that is already archived looks missing on a retry. Pass `archived=true` to `feature-flag-get-all` before you report the flag as missing.\n\nUse `feature-flag-unarchive` to put the flag back in the list, or `delete-feature-flag` when the user wants it gone rather than tidied away.", + "description": "Archive a feature flag, hiding it from the default flag list. Requires the flag's numeric ID; use `feature-flag-get-definition-by-key` to get it from a key, or `feature-flag-get-all` to search by key or name.\n\nSets `archived` to true. An archived flag must be disabled, so an enabled flag is turned off in the same call. Nothing else changes: this tool never accepts or replaces the `filters` object, and linked experiment and survey history is preserved. There is no request body.\n\nWhen the flag is enabled, archiving stops it evaluating everywhere. Tell the user what else this affects: linked experiments, surveys, early access features and session replay settings all appear in the flag's full definition.\n\nReturns 400 when the flag is enabled and other active flags depend on it. Returns 409 when an approval policy gates disabling. Archiving an already-archived flag succeeds and changes nothing, so retrying is safe.\n\nA key lookup that finds nothing does not mean the flag is absent. Archived flags are hidden from `feature-flag-get-definition-by-key` and from `feature-flag-get-all`, so a flag that is already archived looks missing on a retry. Pass `{\"archived\":\"true\"}` to `feature-flag-get-all` before you report the flag as missing.\n\nUse `feature-flag-unarchive` to put the flag back in the list, or `delete-feature-flag` when the user wants it gone rather than tidied away.", "category": "Feature flags", "feature": "flags", "summary": "Archive feature flag", @@ -4836,7 +4836,7 @@ } }, "feature-flag-disable": { - "description": "Turn a feature flag off. Requires the flag's numeric ID; use `feature-flag-get-definition-by-key` to get it from a key, or `feature-flag-get-all` to search by key or name.\n\nSets `active` to false and changes nothing else. This tool never accepts or replaces the `filters` object, so targeting, rollout percentages, variants and payloads cannot change. There is no request body.\n\nA disabled flag stops evaluating everywhere, so every caller falls back to its own default. Tell the user what else this affects: linked experiments, surveys, early access features and session replay settings all appear in the flag's full definition. `feature-flag-get-definition-by-key` already returns it, and `feature-flag-get-definition` returns it by ID.\n\nReturns 400 when other active flags depend on this one; `feature-flags-dependent-flags-retrieve` lists them. Returns 409 when an approval policy gates disabling. Disabling an already-disabled flag succeeds and changes nothing, so retrying is safe.\n\nA key lookup that finds nothing does not mean the flag is absent. Archived flags are hidden from `feature-flag-get-definition-by-key` and from `feature-flag-get-all`, and an archived flag is already disabled. Pass `archived=true` to `feature-flag-get-all` before you report the flag as missing.\n\nTo turn the flag off at a future time instead, use `scheduled-changes-create` with the `update_status` operation.", + "description": "Turn a feature flag off. Requires the flag's numeric ID; use `feature-flag-get-definition-by-key` to get it from a key, or `feature-flag-get-all` to search by key or name.\n\nSets `active` to false and changes nothing else. This tool never accepts or replaces the `filters` object, so targeting, rollout percentages, variants and payloads cannot change. There is no request body.\n\nA disabled flag stops evaluating everywhere, so every caller falls back to its own default. Tell the user what else this affects: linked experiments, surveys, early access features and session replay settings all appear in the flag's full definition. `feature-flag-get-definition-by-key` already returns it, and `feature-flag-get-definition` returns it by ID.\n\nReturns 400 when other active flags depend on this one; `feature-flags-dependent-flags-retrieve` lists them. Returns 409 when an approval policy gates disabling. Disabling an already-disabled flag succeeds and changes nothing, so retrying is safe.\n\nA key lookup that finds nothing does not mean the flag is absent. Archived flags are hidden from `feature-flag-get-definition-by-key` and from `feature-flag-get-all`, and an archived flag is already disabled. Pass `{\"archived\":\"true\"}` to `feature-flag-get-all` before you report the flag as missing.\n\nTo turn the flag off at a future time instead, use `scheduled-changes-create` with the `update_status` operation.", "category": "Feature flags", "feature": "flags", "summary": "Disable feature flag", @@ -4850,7 +4850,7 @@ } }, "feature-flag-enable": { - "description": "Turn a feature flag on. Requires the flag's numeric ID; use `feature-flag-get-definition-by-key` to get it from a key, or `feature-flag-get-all` to search by key or name.\n\nSets `active` to true and changes nothing else. This tool never accepts or replaces the `filters` object, so targeting, rollout percentages, variants and payloads cannot change. There is no request body. To change who the flag is served to, use `update-feature-flag`.\n\nReturns 400 when the flag is archived (call `feature-flag-unarchive` first) or when a flag it depends on is disabled. Returns 409 when an approval policy gates enabling. Enabling an already-enabled flag succeeds and changes nothing, so retrying is safe.\n\nA key lookup that finds nothing does not mean the flag is absent. Archived flags are hidden from `feature-flag-get-definition-by-key` and from `feature-flag-get-all`. Pass `archived=true` to `feature-flag-get-all` before you report the flag as missing.\n\nTo turn the flag on at a future time instead, use `scheduled-changes-create` with the `update_status` operation.", + "description": "Turn a feature flag on. Requires the flag's numeric ID; use `feature-flag-get-definition-by-key` to get it from a key, or `feature-flag-get-all` to search by key or name.\n\nSets `active` to true and changes nothing else. This tool never accepts or replaces the `filters` object, so targeting, rollout percentages, variants and payloads cannot change. There is no request body. To change who the flag is served to, use `update-feature-flag`.\n\nReturns 400 when the flag is archived (call `feature-flag-unarchive` first) or when a flag it depends on is disabled. Returns 409 when an approval policy gates enabling. Enabling an already-enabled flag succeeds and changes nothing, so retrying is safe.\n\nA key lookup that finds nothing does not mean the flag is absent. Archived flags are hidden from `feature-flag-get-definition-by-key` and from `feature-flag-get-all`. Pass `{\"archived\":\"true\"}` to `feature-flag-get-all` before you report the flag as missing.\n\nTo turn the flag on at a future time instead, use `scheduled-changes-create` with the `update_status` operation.", "category": "Feature flags", "feature": "flags", "summary": "Enable feature flag", @@ -4892,7 +4892,7 @@ } }, "feature-flag-unarchive": { - "description": "Restore an archived feature flag to the default flag list. Requires the flag's numeric ID; pass `archived=true` to `feature-flag-get-all` to find archived flags.\n\nSets `archived` to false and changes nothing else. This tool never accepts or replaces the `filters` object. There is no request body.\n\nThe flag stays turned off. Call `feature-flag-enable` when the user also wants it serving again. Unarchiving a flag that is not archived succeeds and changes nothing, so retrying is safe.", + "description": "Restore an archived feature flag to the default flag list. Requires the flag's numeric ID; pass `{\"archived\":\"true\"}` to `feature-flag-get-all` to find archived flags.\n\nSets `archived` to false and changes nothing else. This tool never accepts or replaces the `filters` object. There is no request body.\n\nThe flag stays turned off. Call `feature-flag-enable` when the user also wants it serving again. Unarchiving a flag that is not archived succeeds and changes nothing, so retrying is safe.", "category": "Feature flags", "feature": "flags", "summary": "Unarchive feature flag", diff --git a/services/mcp/schema/tool-definitions-all.json b/services/mcp/schema/tool-definitions-all.json index b5fdeb56829e..8c1f6b1346a4 100644 --- a/services/mcp/schema/tool-definitions-all.json +++ b/services/mcp/schema/tool-definitions-all.json @@ -4977,7 +4977,7 @@ } }, "feature-flag-archive": { - "description": "Archive a feature flag, hiding it from the default flag list. Requires the flag's numeric ID; use `feature-flag-get-definition-by-key` to get it from a key, or `feature-flag-get-all` to search by key or name.\n\nSets `archived` to true. An archived flag must be disabled, so an enabled flag is turned off in the same call. Nothing else changes: this tool never accepts or replaces the `filters` object, and linked experiment and survey history is preserved. There is no request body.\n\nWhen the flag is enabled, archiving stops it evaluating everywhere. Tell the user what else this affects: linked experiments, surveys, early access features and session replay settings all appear in the flag's full definition.\n\nReturns 400 when the flag is enabled and other active flags depend on it. Returns 409 when an approval policy gates disabling. Archiving an already-archived flag succeeds and changes nothing, so retrying is safe.\n\nA key lookup that finds nothing does not mean the flag is absent. Archived flags are hidden from `feature-flag-get-definition-by-key` and from `feature-flag-get-all`, so a flag that is already archived looks missing on a retry. Pass `archived=true` to `feature-flag-get-all` before you report the flag as missing.\n\nUse `feature-flag-unarchive` to put the flag back in the list, or `delete-feature-flag` when the user wants it gone rather than tidied away.", + "description": "Archive a feature flag, hiding it from the default flag list. Requires the flag's numeric ID; use `feature-flag-get-definition-by-key` to get it from a key, or `feature-flag-get-all` to search by key or name.\n\nSets `archived` to true. An archived flag must be disabled, so an enabled flag is turned off in the same call. Nothing else changes: this tool never accepts or replaces the `filters` object, and linked experiment and survey history is preserved. There is no request body.\n\nWhen the flag is enabled, archiving stops it evaluating everywhere. Tell the user what else this affects: linked experiments, surveys, early access features and session replay settings all appear in the flag's full definition.\n\nReturns 400 when the flag is enabled and other active flags depend on it. Returns 409 when an approval policy gates disabling. Archiving an already-archived flag succeeds and changes nothing, so retrying is safe.\n\nA key lookup that finds nothing does not mean the flag is absent. Archived flags are hidden from `feature-flag-get-definition-by-key` and from `feature-flag-get-all`, so a flag that is already archived looks missing on a retry. Pass `{\"archived\":\"true\"}` to `feature-flag-get-all` before you report the flag as missing.\n\nUse `feature-flag-unarchive` to put the flag back in the list, or `delete-feature-flag` when the user wants it gone rather than tidied away.", "category": "Feature flags", "feature": "flags", "summary": "Archive feature flag", @@ -4991,7 +4991,7 @@ } }, "feature-flag-disable": { - "description": "Turn a feature flag off. Requires the flag's numeric ID; use `feature-flag-get-definition-by-key` to get it from a key, or `feature-flag-get-all` to search by key or name.\n\nSets `active` to false and changes nothing else. This tool never accepts or replaces the `filters` object, so targeting, rollout percentages, variants and payloads cannot change. There is no request body.\n\nA disabled flag stops evaluating everywhere, so every caller falls back to its own default. Tell the user what else this affects: linked experiments, surveys, early access features and session replay settings all appear in the flag's full definition. `feature-flag-get-definition-by-key` already returns it, and `feature-flag-get-definition` returns it by ID.\n\nReturns 400 when other active flags depend on this one; `feature-flags-dependent-flags-retrieve` lists them. Returns 409 when an approval policy gates disabling. Disabling an already-disabled flag succeeds and changes nothing, so retrying is safe.\n\nA key lookup that finds nothing does not mean the flag is absent. Archived flags are hidden from `feature-flag-get-definition-by-key` and from `feature-flag-get-all`, and an archived flag is already disabled. Pass `archived=true` to `feature-flag-get-all` before you report the flag as missing.\n\nTo turn the flag off at a future time instead, use `scheduled-changes-create` with the `update_status` operation.", + "description": "Turn a feature flag off. Requires the flag's numeric ID; use `feature-flag-get-definition-by-key` to get it from a key, or `feature-flag-get-all` to search by key or name.\n\nSets `active` to false and changes nothing else. This tool never accepts or replaces the `filters` object, so targeting, rollout percentages, variants and payloads cannot change. There is no request body.\n\nA disabled flag stops evaluating everywhere, so every caller falls back to its own default. Tell the user what else this affects: linked experiments, surveys, early access features and session replay settings all appear in the flag's full definition. `feature-flag-get-definition-by-key` already returns it, and `feature-flag-get-definition` returns it by ID.\n\nReturns 400 when other active flags depend on this one; `feature-flags-dependent-flags-retrieve` lists them. Returns 409 when an approval policy gates disabling. Disabling an already-disabled flag succeeds and changes nothing, so retrying is safe.\n\nA key lookup that finds nothing does not mean the flag is absent. Archived flags are hidden from `feature-flag-get-definition-by-key` and from `feature-flag-get-all`, and an archived flag is already disabled. Pass `{\"archived\":\"true\"}` to `feature-flag-get-all` before you report the flag as missing.\n\nTo turn the flag off at a future time instead, use `scheduled-changes-create` with the `update_status` operation.", "category": "Feature flags", "feature": "flags", "summary": "Disable feature flag", @@ -5005,7 +5005,7 @@ } }, "feature-flag-enable": { - "description": "Turn a feature flag on. Requires the flag's numeric ID; use `feature-flag-get-definition-by-key` to get it from a key, or `feature-flag-get-all` to search by key or name.\n\nSets `active` to true and changes nothing else. This tool never accepts or replaces the `filters` object, so targeting, rollout percentages, variants and payloads cannot change. There is no request body. To change who the flag is served to, use `update-feature-flag`.\n\nReturns 400 when the flag is archived (call `feature-flag-unarchive` first) or when a flag it depends on is disabled. Returns 409 when an approval policy gates enabling. Enabling an already-enabled flag succeeds and changes nothing, so retrying is safe.\n\nA key lookup that finds nothing does not mean the flag is absent. Archived flags are hidden from `feature-flag-get-definition-by-key` and from `feature-flag-get-all`. Pass `archived=true` to `feature-flag-get-all` before you report the flag as missing.\n\nTo turn the flag on at a future time instead, use `scheduled-changes-create` with the `update_status` operation.", + "description": "Turn a feature flag on. Requires the flag's numeric ID; use `feature-flag-get-definition-by-key` to get it from a key, or `feature-flag-get-all` to search by key or name.\n\nSets `active` to true and changes nothing else. This tool never accepts or replaces the `filters` object, so targeting, rollout percentages, variants and payloads cannot change. There is no request body. To change who the flag is served to, use `update-feature-flag`.\n\nReturns 400 when the flag is archived (call `feature-flag-unarchive` first) or when a flag it depends on is disabled. Returns 409 when an approval policy gates enabling. Enabling an already-enabled flag succeeds and changes nothing, so retrying is safe.\n\nA key lookup that finds nothing does not mean the flag is absent. Archived flags are hidden from `feature-flag-get-definition-by-key` and from `feature-flag-get-all`. Pass `{\"archived\":\"true\"}` to `feature-flag-get-all` before you report the flag as missing.\n\nTo turn the flag on at a future time instead, use `scheduled-changes-create` with the `update_status` operation.", "category": "Feature flags", "feature": "flags", "summary": "Enable feature flag", @@ -5061,7 +5061,7 @@ } }, "feature-flag-unarchive": { - "description": "Restore an archived feature flag to the default flag list. Requires the flag's numeric ID; pass `archived=true` to `feature-flag-get-all` to find archived flags.\n\nSets `archived` to false and changes nothing else. This tool never accepts or replaces the `filters` object. There is no request body.\n\nThe flag stays turned off. Call `feature-flag-enable` when the user also wants it serving again. Unarchiving a flag that is not archived succeeds and changes nothing, so retrying is safe.", + "description": "Restore an archived feature flag to the default flag list. Requires the flag's numeric ID; pass `{\"archived\":\"true\"}` to `feature-flag-get-all` to find archived flags.\n\nSets `archived` to false and changes nothing else. This tool never accepts or replaces the `filters` object. There is no request body.\n\nThe flag stays turned off. Call `feature-flag-enable` when the user also wants it serving again. Unarchiving a flag that is not archived succeeds and changes nothing, so retrying is safe.", "category": "Feature flags", "feature": "flags", "summary": "Unarchive feature flag", From 3ef0349506d1ce347530d84e0c42e27d07d3c205 Mon Sep 17 00:00:00 2001 From: James Gill Date: Wed, 2 Sep 2026 15:17:05 +0100 Subject: [PATCH 6/8] fix(flags): return only lifecycle fields to MCP callers A lifecycle tool result carried the whole flag: 1,717 bytes over 32 fields for a flag with two conditions, two variants and one payload, and `filters` alone can reach the 512 KiB cap. All of it lands in the agent's context to confirm a state change, and `filters` in the result reintroduces the targeting object these tools exist to keep out of an agent's hands. `response.include` narrows the four results to id, key, active, archived, status and version. `id` stays because the handler builds the enrich URL from it. The endpoint keeps returning the full representation. Widening a response later needs every client to agree, and only the MCP caller exists so far. Co-Authored-By: Claude Opus 5 (1M context) --- products/feature_flags/mcp/tools.yaml | 32 +++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/products/feature_flags/mcp/tools.yaml b/products/feature_flags/mcp/tools.yaml index 95dae7e41679..c08f9bc475b9 100644 --- a/products/feature_flags/mcp/tools.yaml +++ b/products/feature_flags/mcp/tools.yaml @@ -135,6 +135,14 @@ tools: param_overrides: id: cast: string-int + response: + include: + - id + - key + - active + - archived + - status + - version feature-flag-disable: operation: feature_flags_disable_create enabled: true @@ -176,6 +184,14 @@ tools: param_overrides: id: cast: string-int + response: + include: + - id + - key + - active + - archived + - status + - version feature-flag-enable: operation: feature_flags_enable_create enabled: true @@ -212,6 +228,14 @@ tools: param_overrides: id: cast: string-int + response: + include: + - id + - key + - active + - archived + - status + - version feature-flag-get-all: operation: feature_flags_list enabled: true @@ -287,6 +311,14 @@ tools: param_overrides: id: cast: string-int + response: + include: + - id + - key + - active + - archived + - status + - version feature-flags-activity-retrieve: operation: feature_flags_activity_retrieve enabled: true From 1d764fdafa0b088cf61d7a21e2087d55fdbda5ed Mon Sep 17 00:00:00 2001 From: James Gill Date: Wed, 2 Sep 2026 18:04:44 +0100 Subject: [PATCH 7/8] fix(flags): quote the archived value in the unarchive eval criterion `FeatureFlagsListQueryParams.archived` is a Zod enum of the strings "false" and "true", so a JSON boolean fails schema validation. The criterion said `archived=true`, which an agent can satisfy with a boolean and have the call rejected, while the tool descriptions already show the valid form. The judge could not require a working archived lookup. Co-Authored-By: Claude Opus 5 (1M context) --- services/mcp/evals/benchmark/tasks.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/mcp/evals/benchmark/tasks.yaml b/services/mcp/evals/benchmark/tasks.yaml index 551b7ec539d7..dc293852cb92 100644 --- a/services/mcp/evals/benchmark/tasks.yaml +++ b/services/mcp/evals/benchmark/tasks.yaml @@ -261,7 +261,7 @@ tasks: acceptable_tools: [feature-flag-get-all, feature-flag-get-definition-by-key, feature-flag-get-definition] success_criteria: > Calls feature-flag-unarchive with no filters payload and does not enable it, saying the flag is still turned - off. Finds the archived flag by passing archived=true to feature-flag-get-all; a plain key lookup returning + off. Finds the archived flag by passing {"archived":"true"} to feature-flag-get-all; a plain key lookup returning nothing is not grounds for reporting that the flag does not exist. - id: flag-targeting-edit-uses-update From a857b77d3fd7464787891d4c8f910305b6c75f3a Mon Sep 17 00:00:00 2001 From: James Gill Date: Wed, 2 Sep 2026 18:43:01 +0100 Subject: [PATCH 8/8] chore(flags): regenerate MCP tool handlers after rebase Co-Authored-By: Claude Fable 5 --- .../mcp/src/generated/feature_flags/api.ts | 8 +- .../mcp/src/tools/generated/feature_flags.ts | 128 ++++++++++++++++++ 2 files changed, 132 insertions(+), 4 deletions(-) diff --git a/services/mcp/src/generated/feature_flags/api.ts b/services/mcp/src/generated/feature_flags/api.ts index 9ecc708b50f1..785642981058 100644 --- a/services/mcp/src/generated/feature_flags/api.ts +++ b/services/mcp/src/generated/feature_flags/api.ts @@ -900,7 +900,7 @@ export const FeatureFlagsActivityRetrieveQueryParams = () => zod.object({ * flag is refused when other active flags depend on it. An already-archived flag is * returned unchanged. */ -export const FeatureFlagsArchiveCreateParams = /* @__PURE__ */ zod.object({ +export const FeatureFlagsArchiveCreateParams = () => zod.object({ id: zod.number().describe('A unique integer value identifying this feature flag.'), project_id: zod .string() @@ -931,7 +931,7 @@ export const FeatureFlagsDependentFlagsListParams = () => zod.object({ * A disabled flag stops evaluating for every consumer, including a linked experiment or a * session replay setting. Read the full definition first to report that impact. */ -export const FeatureFlagsDisableCreateParams = /* @__PURE__ */ zod.object({ +export const FeatureFlagsDisableCreateParams = () => zod.object({ id: zod.number().describe('A unique integer value identifying this feature flag.'), project_id: zod .string() @@ -948,7 +948,7 @@ export const FeatureFlagsDisableCreateParams = /* @__PURE__ */ zod.object({ * flag whose own flag dependencies are disabled is also refused. An already-enabled flag * is returned unchanged. */ -export const FeatureFlagsEnableCreateParams = /* @__PURE__ */ zod.object({ +export const FeatureFlagsEnableCreateParams = () => zod.object({ id: zod.number().describe('A unique integer value identifying this feature flag.'), project_id: zod .string() @@ -1011,7 +1011,7 @@ export const FeatureFlagsTestEvaluationCreateBody = () => zod.object({ * Sets `archived` to false and changes nothing else. The flag stays disabled; enable it * with a separate call. An already-unarchived flag is returned unchanged. */ -export const FeatureFlagsUnarchiveCreateParams = /* @__PURE__ */ zod.object({ +export const FeatureFlagsUnarchiveCreateParams = () => zod.object({ id: zod.number().describe('A unique integer value identifying this feature flag.'), project_id: zod .string() diff --git a/services/mcp/src/tools/generated/feature_flags.ts b/services/mcp/src/tools/generated/feature_flags.ts index 430f2d449a04..fff91d492b2b 100644 --- a/services/mcp/src/tools/generated/feature_flags.ts +++ b/services/mcp/src/tools/generated/feature_flags.ts @@ -96,6 +96,99 @@ const deleteFeatureFlag = (): ToolBase { + const FeatureFlagsArchiveCreateParams = orvalSchemas.FeatureFlagsArchiveCreateParams() + return FeatureFlagsArchiveCreateParams.omit({ project_id: true }).extend({ + id: z.preprocess(castStringToInt, FeatureFlagsArchiveCreateParams.shape['id']), + }) +} + +const featureFlagArchive = (): ToolBase< + ReturnType, + WithPostHogUrl +> => ({ + name: 'feature-flag-archive', + schema: FeatureFlagArchiveSchema(), + handler: async (context: Context, params: z.infer>) => { + const projectId = await context.stateManager.getProjectId() + const result = await context.api.request({ + method: 'POST', + path: `/api/projects/${encodeURIComponent(String(projectId))}/feature_flags/${encodeURIComponent(String(params.id))}/archive/`, + }) + const filtered = pickResponseFields(result, [ + 'id', + 'key', + 'active', + 'archived', + 'status', + 'version', + ]) as typeof result + return await withPostHogUrl(context, filtered, `/feature_flags/${filtered.id}`) + }, +}) + +const FeatureFlagDisableSchema = () => { + const FeatureFlagsDisableCreateParams = orvalSchemas.FeatureFlagsDisableCreateParams() + return FeatureFlagsDisableCreateParams.omit({ project_id: true }).extend({ + id: z.preprocess(castStringToInt, FeatureFlagsDisableCreateParams.shape['id']), + }) +} + +const featureFlagDisable = (): ToolBase< + ReturnType, + WithPostHogUrl +> => ({ + name: 'feature-flag-disable', + schema: FeatureFlagDisableSchema(), + handler: async (context: Context, params: z.infer>) => { + const projectId = await context.stateManager.getProjectId() + const result = await context.api.request({ + method: 'POST', + path: `/api/projects/${encodeURIComponent(String(projectId))}/feature_flags/${encodeURIComponent(String(params.id))}/disable/`, + }) + const filtered = pickResponseFields(result, [ + 'id', + 'key', + 'active', + 'archived', + 'status', + 'version', + ]) as typeof result + return await withPostHogUrl(context, filtered, `/feature_flags/${filtered.id}`) + }, +}) + +const FeatureFlagEnableSchema = () => { + const FeatureFlagsEnableCreateParams = orvalSchemas.FeatureFlagsEnableCreateParams() + return FeatureFlagsEnableCreateParams.omit({ project_id: true }).extend({ + id: z.preprocess(castStringToInt, FeatureFlagsEnableCreateParams.shape['id']), + }) +} + +const featureFlagEnable = (): ToolBase< + ReturnType, + WithPostHogUrl +> => ({ + name: 'feature-flag-enable', + schema: FeatureFlagEnableSchema(), + handler: async (context: Context, params: z.infer>) => { + const projectId = await context.stateManager.getProjectId() + const result = await context.api.request({ + method: 'POST', + path: `/api/projects/${encodeURIComponent(String(projectId))}/feature_flags/${encodeURIComponent(String(params.id))}/enable/`, + }) + const filtered = pickResponseFields(result, [ + 'id', + 'key', + 'active', + 'archived', + 'status', + 'version', + ]) as typeof result + return await withPostHogUrl(context, filtered, `/feature_flags/${filtered.id}`) + }, +}) + const FeatureFlagGetAllSchema = () => { const FeatureFlagsListQueryParams = orvalSchemas.FeatureFlagsListQueryParams() return FeatureFlagsListQueryParams.extend({ @@ -177,6 +270,37 @@ const featureFlagGetDefinition = (): ToolBase< }, }) +const FeatureFlagUnarchiveSchema = () => { + const FeatureFlagsUnarchiveCreateParams = orvalSchemas.FeatureFlagsUnarchiveCreateParams() + return FeatureFlagsUnarchiveCreateParams.omit({ project_id: true }).extend({ + id: z.preprocess(castStringToInt, FeatureFlagsUnarchiveCreateParams.shape['id']), + }) +} + +const featureFlagUnarchive = (): ToolBase< + ReturnType, + WithPostHogUrl +> => ({ + name: 'feature-flag-unarchive', + schema: FeatureFlagUnarchiveSchema(), + handler: async (context: Context, params: z.infer>) => { + const projectId = await context.stateManager.getProjectId() + const result = await context.api.request({ + method: 'POST', + path: `/api/projects/${encodeURIComponent(String(projectId))}/feature_flags/${encodeURIComponent(String(params.id))}/unarchive/`, + }) + const filtered = pickResponseFields(result, [ + 'id', + 'key', + 'active', + 'archived', + 'status', + 'version', + ]) as typeof result + return await withPostHogUrl(context, filtered, `/feature_flags/${filtered.id}`) + }, +}) + const FeatureFlagsActivityRetrieveSchema = () => { const FeatureFlagsActivityRetrieveParams = orvalSchemas.FeatureFlagsActivityRetrieveParams() const FeatureFlagsActivityRetrieveQueryParams = orvalSchemas.FeatureFlagsActivityRetrieveQueryParams() @@ -749,8 +873,12 @@ const updateFeatureFlag = (): ToolBase< export const GENERATED_TOOLS: Record ToolBase> = { 'create-feature-flag': createFeatureFlag, 'delete-feature-flag': deleteFeatureFlag, + 'feature-flag-archive': featureFlagArchive, + 'feature-flag-disable': featureFlagDisable, + 'feature-flag-enable': featureFlagEnable, 'feature-flag-get-all': featureFlagGetAll, 'feature-flag-get-definition': featureFlagGetDefinition, + 'feature-flag-unarchive': featureFlagUnarchive, 'feature-flags-activity-retrieve': featureFlagsActivityRetrieve, 'feature-flags-bulk-delete-create': featureFlagsBulkDeleteCreate, 'feature-flags-bulk-keys-retrieve': featureFlagsBulkKeysRetrieve,