harness-mcp-v2 3.2.19 → 3.2.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.md +146 -75
  2. package/build/client/harness-client.d.ts.map +1 -1
  3. package/build/client/harness-client.js +32 -6
  4. package/build/client/harness-client.js.map +1 -1
  5. package/build/config.d.ts +10 -0
  6. package/build/config.d.ts.map +1 -1
  7. package/build/config.js +15 -0
  8. package/build/config.js.map +1 -1
  9. package/build/data/schemas/v0/pipeline.d.ts.map +1 -1
  10. package/build/data/schemas/v0/pipeline.js +1388 -580
  11. package/build/data/schemas/v0/pipeline.js.map +1 -1
  12. package/build/data/schemas/v0/template.d.ts.map +1 -1
  13. package/build/data/schemas/v0/template.js +1142 -166
  14. package/build/data/schemas/v0/template.js.map +1 -1
  15. package/build/data/schemas/v1/pipeline.d.ts.map +1 -1
  16. package/build/data/schemas/v1/pipeline.js +302 -90
  17. package/build/data/schemas/v1/pipeline.js.map +1 -1
  18. package/build/data/schemas/v1/template.d.ts.map +1 -1
  19. package/build/data/schemas/v1/template.js +708 -488
  20. package/build/data/schemas/v1/template.js.map +1 -1
  21. package/build/prompts/deploy-ai-agent.d.ts +3 -0
  22. package/build/prompts/deploy-ai-agent.d.ts.map +1 -0
  23. package/build/prompts/deploy-ai-agent.js +300 -0
  24. package/build/prompts/deploy-ai-agent.js.map +1 -0
  25. package/build/prompts/feature-flag-rollout.d.ts.map +1 -1
  26. package/build/prompts/feature-flag-rollout.js +19 -8
  27. package/build/prompts/feature-flag-rollout.js.map +1 -1
  28. package/build/prompts/index.d.ts.map +1 -1
  29. package/build/prompts/index.js +2 -0
  30. package/build/prompts/index.js.map +1 -1
  31. package/build/registry/extractors.d.ts +87 -10
  32. package/build/registry/extractors.d.ts.map +1 -1
  33. package/build/registry/extractors.js +426 -26
  34. package/build/registry/extractors.js.map +1 -1
  35. package/build/registry/index.d.ts.map +1 -1
  36. package/build/registry/index.js +34 -15
  37. package/build/registry/index.js.map +1 -1
  38. package/build/registry/scope-utils.d.ts +45 -0
  39. package/build/registry/scope-utils.d.ts.map +1 -1
  40. package/build/registry/scope-utils.js +78 -0
  41. package/build/registry/scope-utils.js.map +1 -1
  42. package/build/registry/toolsets/chaos-descriptions.d.ts +72 -20
  43. package/build/registry/toolsets/chaos-descriptions.d.ts.map +1 -1
  44. package/build/registry/toolsets/chaos-descriptions.js +435 -140
  45. package/build/registry/toolsets/chaos-descriptions.js.map +1 -1
  46. package/build/registry/toolsets/chaos.d.ts.map +1 -1
  47. package/build/registry/toolsets/chaos.js +967 -193
  48. package/build/registry/toolsets/chaos.js.map +1 -1
  49. package/build/registry/toolsets/feature-flags.d.ts.map +1 -1
  50. package/build/registry/toolsets/feature-flags.js +888 -85
  51. package/build/registry/toolsets/feature-flags.js.map +1 -1
  52. package/build/registry/toolsets/governance.d.ts.map +1 -1
  53. package/build/registry/toolsets/governance.js +4 -0
  54. package/build/registry/toolsets/governance.js.map +1 -1
  55. package/build/registry/toolsets/iacm.d.ts.map +1 -1
  56. package/build/registry/toolsets/iacm.js +787 -11
  57. package/build/registry/toolsets/iacm.js.map +1 -1
  58. package/build/registry/toolsets/infrastructure.d.ts.map +1 -1
  59. package/build/registry/toolsets/infrastructure.js +30 -5
  60. package/build/registry/toolsets/infrastructure.js.map +1 -1
  61. package/build/registry/toolsets/pipelines.d.ts.map +1 -1
  62. package/build/registry/toolsets/pipelines.js +208 -5
  63. package/build/registry/toolsets/pipelines.js.map +1 -1
  64. package/build/registry/toolsets/release-management.d.ts +8 -0
  65. package/build/registry/toolsets/release-management.d.ts.map +1 -0
  66. package/build/registry/toolsets/release-management.js +783 -0
  67. package/build/registry/toolsets/release-management.js.map +1 -0
  68. package/build/registry/toolsets/sto.d.ts.map +1 -1
  69. package/build/registry/toolsets/sto.js +93 -17
  70. package/build/registry/toolsets/sto.js.map +1 -1
  71. package/build/registry/types.d.ts +52 -4
  72. package/build/registry/types.d.ts.map +1 -1
  73. package/build/resources/harness-schema.d.ts.map +1 -1
  74. package/build/resources/harness-schema.js +2 -1
  75. package/build/resources/harness-schema.js.map +1 -1
  76. package/build/tools/entity-schema/live.d.ts +6 -5
  77. package/build/tools/entity-schema/live.d.ts.map +1 -1
  78. package/build/tools/entity-schema/live.js +114 -10
  79. package/build/tools/entity-schema/live.js.map +1 -1
  80. package/build/tools/entity-schema/types.d.ts +7 -1
  81. package/build/tools/entity-schema/types.d.ts.map +1 -1
  82. package/build/tools/harness-create.d.ts.map +1 -1
  83. package/build/tools/harness-create.js +5 -1
  84. package/build/tools/harness-create.js.map +1 -1
  85. package/build/tools/harness-delete.d.ts.map +1 -1
  86. package/build/tools/harness-delete.js +5 -1
  87. package/build/tools/harness-delete.js.map +1 -1
  88. package/build/tools/harness-execute.d.ts.map +1 -1
  89. package/build/tools/harness-execute.js +15 -9
  90. package/build/tools/harness-execute.js.map +1 -1
  91. package/build/tools/harness-get.d.ts.map +1 -1
  92. package/build/tools/harness-get.js +5 -1
  93. package/build/tools/harness-get.js.map +1 -1
  94. package/build/tools/harness-list.d.ts.map +1 -1
  95. package/build/tools/harness-list.js +18 -2
  96. package/build/tools/harness-list.js.map +1 -1
  97. package/build/tools/harness-schema.d.ts +2 -1
  98. package/build/tools/harness-schema.d.ts.map +1 -1
  99. package/build/tools/harness-schema.js +10 -8
  100. package/build/tools/harness-schema.js.map +1 -1
  101. package/build/tools/harness-update.d.ts.map +1 -1
  102. package/build/tools/harness-update.js +5 -1
  103. package/build/tools/harness-update.js.map +1 -1
  104. package/build/tools/index.js +1 -1
  105. package/build/tools/index.js.map +1 -1
  106. package/build/utils/url-parser.d.ts +2 -0
  107. package/build/utils/url-parser.d.ts.map +1 -1
  108. package/build/utils/url-parser.js +55 -0
  109. package/build/utils/url-parser.js.map +1 -1
  110. package/npm-shrinkwrap.json +2 -2
  111. package/package.json +1 -1
@@ -1,24 +1,105 @@
1
- import { passthrough, fmeListExtract, fmeGetExtract } from "../extractors.js";
1
+ import { passthrough, fmeListExtract, fmeGetExtract, fmeV4PaginatedListExtract } from "../extractors.js";
2
+ import { isFmeHarnessNativeSelected, logFmeDeprecation, requireFmeIdentifier, requireHarnessNativeSegmentScope, resolveFmeDualMode } from "../scope-utils.js";
2
3
  const fmeActionExtract = (raw) => {
3
4
  if (raw !== null && typeof raw === "object" && !Array.isArray(raw))
4
5
  return raw;
5
6
  return { success: true, result: raw };
6
7
  };
8
+ // The confirmed Harness-native FME v4 API (/fme/api/v4/...) does not use the
9
+ // standard NG orgIdentifier/projectIdentifier query param convention — it expects
10
+ // account_id/organization_identifier/project_identifier instead (confirmed live
11
+ // against qa.harness.io). Applied per-route (never resource-level) on dual-mode
12
+ // resources so the legacy Split.io branch's wire format stays untouched.
13
+ const FME_HARNESS_NATIVE_SCOPE_PARAMS = {
14
+ account: "account_id",
15
+ org: "organization_identifier",
16
+ project: "project_identifier",
17
+ };
18
+ // Harness-native v4 create bodies (CreateFeatureFlagRequest/CreateSegmentRequest,
19
+ // confirmed against Harness_Split/Main) accept tags as [{name: string}]; callers
20
+ // commonly pass bare strings instead, so normalize the same way update already does.
21
+ function normalizeFmeTags(tags) {
22
+ return Array.isArray(tags) ? tags.map((t) => (typeof t === "string" ? { name: t } : t)) : tags;
23
+ }
24
+ const FME_SEGMENT_TYPES = ["standard", "rule_based", "large"];
7
25
  const fmeFeatureFlagUpdateSchema = {
8
- description: "Partial update for an FME feature flag's metadata. Provide the fields you want to change they are converted to JSON Patch (RFC 6902) operations automatically.",
26
+ description: "Partial update for an FME feature flag's metadata. Provide the fields you want to change. Legacy mode (workspace_id): converted to JSON Patch (RFC 6902) automatically — description/tags/rolloutStatus only. Harness-native mode (org_id+project_id): sent as JSON Merge Patch (RFC 7396) — description/tags/owners/rolloutStatus; set description/tags/owners to null (or [] for tags/owners) to clear them, omit a field to leave it unchanged.",
9
27
  fields: [
10
28
  { name: "description", type: "string", required: false, description: "Updated description" },
11
29
  { name: "tags", type: "array", required: false, description: "Updated tags — provide as [{name: 'tag1'}] or ['tag1', 'tag2'] (strings are auto-wrapped)", itemType: "object" },
30
+ { name: "owners", type: "array", required: false, description: "Harness-native mode only. Updated owners — each entry is {type: \"USER\", id or email} or {type: \"GROUP\", identifier}", itemType: "object" },
12
31
  { name: "rolloutStatus", type: "object", required: false, description: "Rollout status — provide as {id: '<uuid>'} (use fme_rollout_status to discover valid IDs)" },
13
32
  ],
14
33
  };
15
34
  const fmeFeatureFlagCreateSchema = {
16
- description: "Create a new feature flag (split) in a workspace under a specific traffic type. Note: the Split API does not support tags on create use harness_update to add tags after creation.",
35
+ description: "Create a new feature flag. Legacy mode (workspace_id + traffic_type_id): name required, description optional tags/owners not supported on create (the Split API has no create-time support; use harness_update after creation). Harness-native mode (org_id+project_id): name and trafficType required, description/tags/owners optional.",
17
36
  fields: [
18
- { name: "name", type: "string", required: true, description: "Feature flag name (must be unique within the workspace)" },
37
+ { name: "name", type: "string", required: true, description: "Feature flag name (must be unique within the workspace/project)" },
19
38
  { name: "description", type: "string", required: false, description: "Optional description of the feature flag" },
39
+ { name: "trafficType", type: "string", required: false, description: "Traffic type name. Required in Harness-native (org_id+project_id) mode; also accepted as top-level traffic_type_id or traffic_type. Ignored in legacy mode (pass traffic_type_id as a path param there instead)" },
40
+ { name: "tags", type: "array", required: false, description: "Harness-native mode only. Each entry is {name: string}; bare strings are accepted and auto-wrapped", itemType: "object" },
41
+ { name: "owners", type: "array", required: false, description: "Harness-native mode only. Each entry is {type: \"USER\", id or email} or {type: \"GROUP\", identifier}", itemType: "object" },
42
+ ],
43
+ };
44
+ const fmeFeatureFlagKillRestoreReallocateSchema = {
45
+ description: "Optional comment and/or title recorded with the action. Both fields are accepted in legacy (workspace_id) and Harness-native (org_id+project_id) mode.",
46
+ fields: [
47
+ { name: "comment", type: "string", required: false, description: "Optional comment explaining the change" },
48
+ { name: "title", type: "string", required: false, description: "Optional short title for the change" },
49
+ ],
50
+ };
51
+ const fmeFeatureFlagArchiveSchema = {
52
+ description: "Optional comment recorded with the action.",
53
+ fields: [
54
+ { name: "comment", type: "string", required: false, description: "Optional comment explaining the change" },
55
+ ],
56
+ };
57
+ const fmeEnvironmentCreateSchema = {
58
+ description: "Create an FME environment (Harness-native org_id+project_id only). name is required (max 15 characters). isProduction is optional (default false). production is accepted as an alias for isProduction.",
59
+ fields: [
60
+ { name: "name", type: "string", required: true, description: "Environment name (unique in the project; max 15 characters)" },
61
+ { name: "isProduction", type: "boolean", required: false, description: "Whether this is a production environment. Optional; defaults to false on the backend if omitted." },
62
+ { name: "production", type: "boolean", required: false, description: "Alias for isProduction." },
20
63
  ],
21
64
  };
65
+ const fmeEnvironmentUpdateSchema = {
66
+ description: "Partial environment update via JSON Merge Patch (RFC 7396), Harness-native only. name and isProduction are not clearable — omit to leave unchanged. production is accepted as an alias for isProduction.",
67
+ fields: [
68
+ { name: "name", type: "string", required: false, description: "Updated name; omit to leave unchanged. Blank names are rejected. Not clearable." },
69
+ { name: "isProduction", type: "boolean", required: false, description: "Updated production flag. Omit to leave unchanged; not clearable." },
70
+ { name: "production", type: "boolean", required: false, description: "Alias for isProduction." },
71
+ ],
72
+ };
73
+ function fmeEnvironmentProduction(body) {
74
+ if (!body)
75
+ return undefined;
76
+ if (body.isProduction !== undefined && body.isProduction !== null)
77
+ return Boolean(body.isProduction);
78
+ if (body.production !== undefined && body.production !== null)
79
+ return Boolean(body.production);
80
+ return undefined;
81
+ }
82
+ /** MCP never had workspace_id get/create/update/delete for environments (#806 list-only). */
83
+ function resolveNativeOnlyEnvironmentRoute(input, operation, opts = {}) {
84
+ const mode = resolveFmeDualMode(input, "fme_environment");
85
+ if (mode.mode === "legacy") {
86
+ throw new Error(`fme_environment.${operation}: Harness-native (org_id/project_id) only — MCP never supported workspace_id for this operation (list remains dual-mode).`);
87
+ }
88
+ if (opts.collection) {
89
+ return {
90
+ path: "/fme/api/v4/environments",
91
+ product: "harness",
92
+ scopeParams: FME_HARNESS_NATIVE_SCOPE_PARAMS,
93
+ };
94
+ }
95
+ const environmentId = encodeURIComponent(requireFmeIdentifier(input, "environment_id", "fme_environment"));
96
+ return {
97
+ path: `/fme/api/v4/environments/${environmentId}`,
98
+ product: "harness",
99
+ scopeParams: FME_HARNESS_NATIVE_SCOPE_PARAMS,
100
+ ...(opts.mergePatch ? { headers: { "Content-Type": "application/merge-patch+json" } } : {}),
101
+ };
102
+ }
22
103
  const fmeFeatureFlagDefinitionCreateSchema = {
23
104
  description: "Create a feature flag definition in a specific environment (initial treatments, rules, and default rule required)",
24
105
  fields: [
@@ -29,6 +110,7 @@ const fmeFeatureFlagDefinitionCreateSchema = {
29
110
  { name: "baselineTreatment", type: "string", required: false, description: "The baseline (control) treatment for experimentation" },
30
111
  { name: "trafficAllocation", type: "number", required: false, description: "Percentage of traffic to include (0–100)" },
31
112
  { name: "comment", type: "string", required: false, description: "Comment describing the change" },
113
+ { name: "title", type: "string", required: false, description: "Harness-native mode only. Optional definition title." },
32
114
  ],
33
115
  };
34
116
  const fmeFeatureFlagDefinitionUpdateSchema = {
@@ -41,6 +123,7 @@ const fmeFeatureFlagDefinitionUpdateSchema = {
41
123
  { name: "defaultRule", type: "array", required: false, description: "Default rule buckets (treatment/size pairs) applied when no targeting rules match", itemType: "object" },
42
124
  { name: "trafficAllocation", type: "number", required: false, description: "Percentage of traffic to include (0–100)" },
43
125
  { name: "comment", type: "string", required: false, description: "Comment describing the change" },
126
+ { name: "title", type: "string", required: false, description: "Harness-native mode only. Optional definition title." },
44
127
  ],
45
128
  };
46
129
  const fmeRbsCreateSchema = {
@@ -50,6 +133,25 @@ const fmeRbsCreateSchema = {
50
133
  { name: "description", type: "string", required: false, description: "Optional segment description" },
51
134
  ],
52
135
  };
136
+ const fmeSegmentCreateSchema = {
137
+ description: "Create a new segment. name, trafficType, and type are required; description/tags/owners are optional.",
138
+ fields: [
139
+ { name: "name", type: "string", required: true, description: "Segment name (must be unique within the project)" },
140
+ { name: "type", type: "string", required: true, description: "Segment kind. Must be one of: \"standard\" | \"rule_based\" | \"large\"." },
141
+ { name: "description", type: "string", required: false, description: "Optional description of the segment" },
142
+ { name: "trafficType", type: "string", required: true, description: "Traffic type name" },
143
+ { name: "tags", type: "array", required: false, description: "Each entry is {name: string}; bare strings are accepted and auto-wrapped", itemType: "object" },
144
+ { name: "owners", type: "array", required: false, description: "Each entry is {type: \"USER\", id or email} or {type: \"GROUP\", identifier}", itemType: "object" },
145
+ ],
146
+ };
147
+ const fmeSegmentDefinitionCreateSchema = {
148
+ description: "Create a segment definition in an environment. Omit the body (or send {}) to create an empty/default shell.",
149
+ fields: [{ name: "description", type: "string", required: false, description: "Optional description for the definition" }],
150
+ };
151
+ const fmeSegmentDefinitionUpdateSchema = {
152
+ description: "Update a segment definition via JSON Merge Patch (RFC 7396). description is the only mutable field: omit it to leave unchanged, pass null to clear it, or a string to set it.",
153
+ fields: [{ name: "description", type: "string", required: false, description: "Omit to keep the current value, null to clear it, or a string to set it" }],
154
+ };
53
155
  const fmeRbsUpdateDefinitionSchema = {
54
156
  description: "Update a rule-based segment definition in an environment. Rules use: {condition: {combiner: 'AND', matchers: [{type, attribute, ...}]}}. Matcher types: IN_LIST_STRING (strings:[]), GREATER_THAN_OR_EQUAL_NUMBER (number:N), LESS_THAN_OR_EQUAL_NUMBER (number:N), BETWEEN_NUMBER (between:{from,to}), BOOLEAN (bool:true/false), ON_DATE (date:ms), IN_SPLIT (depends:{splitName,treatment}). Combiner values: AND, OR.",
55
157
  fields: [
@@ -92,10 +194,35 @@ const fmeRbsChangeRequestSchema = {
92
194
  },
93
195
  ],
94
196
  };
197
+ function resolveNativeOnlyDefinitionRoute(input, operation, opts = {}) {
198
+ const mode = resolveFmeDualMode(input, "fme_feature_flag_definition");
199
+ if (mode.mode === "legacy") {
200
+ throw new Error(`fme_feature_flag_definition.${operation}: Harness-native (org_id/project_id) only — pass org_id+project_id instead of workspace_id.`);
201
+ }
202
+ if (opts.collection) {
203
+ requireFmeIdentifier(input, "feature_flag_name", "fme_feature_flag_definition");
204
+ return {
205
+ path: "/fme/api/v4/feature-flag-definitions",
206
+ product: "harness",
207
+ scopeParams: FME_HARNESS_NATIVE_SCOPE_PARAMS,
208
+ };
209
+ }
210
+ const flagName = encodeURIComponent(requireFmeIdentifier(input, "feature_flag_name", "fme_feature_flag_definition"));
211
+ requireFmeIdentifier(input, "environment_id", "fme_feature_flag_definition");
212
+ return {
213
+ path: `/fme/api/v4/feature-flag-definitions/${flagName}${opts.suffix ?? ""}`,
214
+ product: "harness",
215
+ scopeParams: FME_HARNESS_NATIVE_SCOPE_PARAMS,
216
+ };
217
+ }
95
218
  export const featureFlagsToolset = {
96
219
  name: "feature-flags",
97
220
  displayName: "Feature Management & Experimentation",
98
- description: "Harness FME — feature flags, rule-based segments, workspaces, environments, and rollout statuses via the Split.io API",
221
+ description: "Harness FME — feature flags, segments, environments, and rollout statuses. " +
222
+ "Two mutually exclusive scoping modes: legacy workspace_id (Split.io API) or " +
223
+ "Harness-native org_id+project_id. If you already have org_id and project_id, use " +
224
+ "them directly and skip fme_workspace entirely — org/project fully identify scope " +
225
+ "on their own; there is no need to look up a workspace first.",
99
226
  resources: [
100
227
  // ── FME Resources (Split.io API at https://api.split.io) ───────────
101
228
  // These use account scope to avoid injecting orgIdentifier/projectIdentifier
@@ -104,9 +231,14 @@ export const featureFlagsToolset = {
104
231
  {
105
232
  resourceType: "fme_workspace",
106
233
  displayName: "FME Workspace",
107
- description: "Feature Management workspace. Supports list with pagination (offset/size, default 20, max 1000).",
234
+ description: "Feature Management workspace legacy Split.io concept only, used solely to discover a " +
235
+ "workspace_id for other fme_* resources' deprecated legacy calls. Not used by Harness-native " +
236
+ "calls at all: if you already have org_id and project_id, skip this resource entirely and " +
237
+ "pass them directly to the fme_* resource you actually want. Supports list with pagination " +
238
+ "(offset/size, default 20, max 1000).",
108
239
  toolset: "feature-flags",
109
240
  scope: "account",
241
+ scopeOptional: true,
110
242
  identifierFields: ["workspace_id"],
111
243
  product: "fme",
112
244
  listFilterFields: [
@@ -116,6 +248,13 @@ export const featureFlagsToolset = {
116
248
  list: {
117
249
  method: "GET",
118
250
  path: "/internal/api/v2/workspaces",
251
+ routeResolver: (input) => {
252
+ if (isFmeHarnessNativeSelected(input, "fme_workspace.list")) {
253
+ throw new Error("fme_workspace: this resource has no Harness-native equivalent — it exists only to discover workspace_id values for the deprecated legacy contract.");
254
+ }
255
+ logFmeDeprecation("[DEPRECATION] fme_workspace: this resource and the workspace_id contract it supports are deprecated.");
256
+ return { path: "/internal/api/v2/workspaces" };
257
+ },
119
258
  operationPolicy: { risk: "read", retryPolicy: "safe" },
120
259
  queryParams: {
121
260
  offset: "offset",
@@ -129,37 +268,104 @@ export const featureFlagsToolset = {
129
268
  {
130
269
  resourceType: "fme_environment",
131
270
  displayName: "FME Environment",
132
- description: "Feature Management environment. Supports list. Requires a workspace_id.",
271
+ description: "Feature Management environment. Dual-mode list (workspace_id or org_id+project_id). get/create/update/delete are Harness-native only. Native create/update use isProduction (production accepted as an alias). Native PATCH is JSON Merge Patch; name and isProduction are not clearable. Name max 15 characters. Delete returns 400 hasDependents while SDK API keys (always created with a new env), flags, or segments remain.",
133
272
  toolset: "feature-flags",
134
273
  scope: "account",
274
+ scopeOptional: true,
135
275
  identifierFields: ["workspace_id", "environment_id"],
136
276
  product: "fme",
137
- baseUrlOverride: "fme",
138
277
  listFilterFields: [
139
- { name: "workspace_id", description: "FME workspace ID (get from harness_list resource_type=fme_workspace)", required: true },
278
+ { name: "workspace_id", description: "FME workspace ID (get from harness_list resource_type=fme_workspace). Deprecated omit and pass org_id+project_id instead for Harness-native scoping." },
279
+ { name: "offset", description: "Harness-native pagination offset (default 0)", type: "number" },
280
+ { name: "limit", description: "Harness-native page size (default 100, max 100)", type: "number" },
140
281
  ],
141
282
  operations: {
142
283
  list: {
143
284
  method: "GET",
144
- path: "/internal/api/v2/environments/ws/{wsId}",
285
+ path: "",
286
+ routeResolver: (input) => {
287
+ const mode = resolveFmeDualMode(input, "fme_environment");
288
+ if (mode.mode === "legacy") {
289
+ return { path: `/internal/api/v2/environments/ws/${encodeURIComponent(mode.workspaceId)}` };
290
+ }
291
+ return { path: "/fme/api/v4/environments", product: "harness", scopeParams: FME_HARNESS_NATIVE_SCOPE_PARAMS };
292
+ },
145
293
  operationPolicy: { risk: "read", retryPolicy: "safe" },
146
- pathParams: { workspace_id: "wsId" },
294
+ queryParams: { offset: "offset", size: "limit", limit: "limit" },
295
+ responseExtractor: fmeV4PaginatedListExtract,
296
+ description: "List FME environments for a workspace (legacy) or org_id+project_id project (Harness-native). Native envelope {data, limit, offset, totalCount} is promoted to items/total; harness_list size maps to limit.",
297
+ },
298
+ get: {
299
+ method: "GET",
300
+ path: "",
301
+ routeResolver: (input) => resolveNativeOnlyEnvironmentRoute(input, "get"),
302
+ operationPolicy: { risk: "read", retryPolicy: "safe" },
303
+ responseExtractor: passthrough,
304
+ description: "Get a single environment by environment_id (UUID from list). Harness-native only (org_id+project_id). MCP never supported workspace_id get.",
305
+ },
306
+ create: {
307
+ method: "POST",
308
+ path: "",
309
+ routeResolver: (input) => resolveNativeOnlyEnvironmentRoute(input, "create", { collection: true }),
310
+ operationPolicy: { risk: "low_write", retryPolicy: "do_not_retry" },
311
+ skipScopeBodyInjection: true,
312
+ bodyBuilder: (input) => {
313
+ const body = input.body;
314
+ const production = fmeEnvironmentProduction(body);
315
+ return {
316
+ name: body?.name,
317
+ ...(production !== undefined ? { isProduction: production } : {}),
318
+ };
319
+ },
147
320
  responseExtractor: passthrough,
148
- description: "List FME environments for a workspace",
321
+ bodySchema: fmeEnvironmentCreateSchema,
322
+ description: "Create an environment (Harness-native only). Body requires name (max 15 characters). Optional isProduction (CreateEnvironmentRequest). NG scope is not injected into the JSON.",
323
+ },
324
+ update: {
325
+ method: "PATCH",
326
+ path: "",
327
+ routeResolver: (input) => resolveNativeOnlyEnvironmentRoute(input, "update", { mergePatch: true }),
328
+ operationPolicy: { risk: "low_write", retryPolicy: "safe" },
329
+ skipScopeBodyInjection: true,
330
+ bodyBuilder: (input) => {
331
+ const body = input.body;
332
+ if (!body)
333
+ return {};
334
+ const production = fmeEnvironmentProduction(body);
335
+ return {
336
+ ...(typeof body.name === "string" ? { name: body.name } : {}),
337
+ ...(production !== undefined ? { isProduction: production } : {}),
338
+ };
339
+ },
340
+ responseExtractor: passthrough,
341
+ bodySchema: fmeEnvironmentUpdateSchema,
342
+ description: "Update an environment by environment_id (Harness-native only). JSON Merge Patch on name and isProduction (not clearable).",
343
+ },
344
+ delete: {
345
+ method: "DELETE",
346
+ path: "",
347
+ routeResolver: (input) => resolveNativeOnlyEnvironmentRoute(input, "delete"),
348
+ operationPolicy: { risk: "destructive", retryPolicy: "do_not_retry" },
349
+ responseExtractor: passthrough,
350
+ description: "Delete (archive) an environment by environment_id (Harness-native only). Returns 400 hasDependents while SDK API keys, flags, or segments still target it. Create via EnvironmentStarterKit always provisions client/server API keys, so a brand-new environment cannot be deleted until those keys are removed.",
149
351
  },
150
352
  },
151
353
  },
152
354
  {
153
355
  resourceType: "fme_feature_flag",
154
356
  displayName: "FME Feature Flag",
155
- description: "Feature flag via the Split.io API. List flags by workspace with filtering (name, tags, rollout_status_id) and pagination (offset/size, default 20, max 50). Supports create (requires traffic_type_id), get, delete, update, and kill/restore/archive/unarchive execute actions.",
357
+ description: "Feature flag. Dual-mode scoping: pass org_id+project_id (Harness-native, preferred no " +
358
+ "workspace lookup needed) or the deprecated workspace_id (Split.io API). Both modes support " +
359
+ "list/get/create/delete/update/kill/restore/reallocate/archive/unarchive. List supports " +
360
+ "filtering (name, tags, rollout_status_id) and pagination (offset/size, default 20, max 50).",
156
361
  toolset: "feature-flags",
157
362
  scope: "account",
363
+ scopeOptional: true,
158
364
  identifierFields: ["workspace_id", "feature_flag_name"],
159
365
  product: "fme",
160
366
  deepLinkTemplate: "/ng/account/{accountId}/module/fme/orgs/{orgIdentifier}/projects/{projectIdentifier}/setup/resources/targets/{trafficTypeId}/splits/{id}",
161
367
  listFilterFields: [
162
- { name: "workspace_id", description: "FME workspace ID (get from harness_list resource_type=fme_workspace)", required: true },
368
+ { name: "workspace_id", description: "FME workspace ID (get from harness_list resource_type=fme_workspace). Deprecated omit and pass org_id+project_id instead for Harness-native scoping." },
163
369
  { name: "offset", description: "Pagination offset for FME feature flags", type: "number" },
164
370
  { name: "rollout_status_id", description: "Filter by rollout status UUID (use fme_rollout_status to discover valid IDs)", type: "string" },
165
371
  { name: "name", description: "Filter flags by name (partial match)", type: "string" },
@@ -168,9 +374,15 @@ export const featureFlagsToolset = {
168
374
  operations: {
169
375
  list: {
170
376
  method: "GET",
171
- path: "/internal/api/v2/splits/ws/{wsId}",
377
+ path: "",
378
+ routeResolver: (input) => {
379
+ const mode = resolveFmeDualMode(input, "fme_feature_flag");
380
+ if (mode.mode === "legacy") {
381
+ return { path: `/internal/api/v2/splits/ws/${encodeURIComponent(mode.workspaceId)}` };
382
+ }
383
+ return { path: "/fme/api/v4/feature-flags", product: "harness", scopeParams: FME_HARNESS_NATIVE_SCOPE_PARAMS };
384
+ },
172
385
  operationPolicy: { risk: "read", retryPolicy: "safe" },
173
- pathParams: { workspace_id: "wsId" },
174
386
  queryParams: {
175
387
  offset: "offset",
176
388
  size: "limit",
@@ -179,23 +391,54 @@ export const featureFlagsToolset = {
179
391
  tags: "tag",
180
392
  },
181
393
  responseExtractor: fmeListExtract,
182
- description: "List feature flags for a workspace with filtering and pagination (offset and size params, max 50).",
394
+ description: "List feature flags by workspace_id (legacy, deprecated) or org_id+project_id (Harness-native), with filtering and pagination (offset and size params, max 50).",
183
395
  },
184
396
  get: {
185
397
  method: "GET",
186
- path: "/internal/api/v2/splits/ws/{wsId}/{featureFlagName}",
398
+ path: "",
399
+ routeResolver: (input) => {
400
+ const mode = resolveFmeDualMode(input, "fme_feature_flag");
401
+ const flagName = encodeURIComponent(requireFmeIdentifier(input, "feature_flag_name", "fme_feature_flag"));
402
+ if (mode.mode === "legacy") {
403
+ return { path: `/internal/api/v2/splits/ws/${encodeURIComponent(mode.workspaceId)}/${flagName}` };
404
+ }
405
+ return { path: `/fme/api/v4/feature-flags/${flagName}`, product: "harness", scopeParams: FME_HARNESS_NATIVE_SCOPE_PARAMS };
406
+ },
187
407
  operationPolicy: { risk: "read", retryPolicy: "safe" },
188
- pathParams: { workspace_id: "wsId", feature_flag_name: "featureFlagName" },
189
408
  responseExtractor: fmeGetExtract,
190
- description: "Get a specific feature flag's metadata without requiring an environment",
409
+ description: "Get a specific feature flag's metadata without requiring an environment (legacy: workspace_id; Harness-native: org_id+project_id).",
191
410
  },
192
411
  create: {
193
412
  method: "POST",
194
413
  path: "/internal/api/v2/splits/ws/{wsId}/trafficTypes/{trafficTypeId}",
414
+ routeResolver: (input) => {
415
+ const mode = resolveFmeDualMode(input, "fme_feature_flag");
416
+ if (mode.mode === "harness_native") {
417
+ return { path: "/fme/api/v4/feature-flags", product: "harness", scopeParams: FME_HARNESS_NATIVE_SCOPE_PARAMS };
418
+ }
419
+ return { path: `/internal/api/v2/splits/ws/${encodeURIComponent(mode.workspaceId)}/trafficTypes/${encodeURIComponent(requireFmeIdentifier(input, "traffic_type_id", "fme_feature_flag"))}` };
420
+ },
195
421
  operationPolicy: { risk: "low_write", retryPolicy: "do_not_retry" },
196
422
  pathParams: { workspace_id: "wsId", traffic_type_id: "trafficTypeId" },
197
423
  bodyBuilder: (input) => {
198
424
  const body = input.body;
425
+ if (isFmeHarnessNativeSelected(input, "fme_feature_flag")) {
426
+ const trafficType = (body?.trafficType ?? input.traffic_type_id ?? input.traffic_type ?? body?.traffic_type);
427
+ if (!trafficType) {
428
+ throw new Error('fme_feature_flag.create: "trafficType" is required in Harness-native mode — pass body.trafficType, traffic_type_id, or traffic_type.');
429
+ }
430
+ const name = body?.name ?? input.name;
431
+ if (!name) {
432
+ throw new Error('fme_feature_flag.create: "name" is required in Harness-native mode — pass body.name.');
433
+ }
434
+ return {
435
+ name,
436
+ trafficType,
437
+ ...(body?.description !== undefined ? { description: body.description } : {}),
438
+ ...(body?.tags !== undefined ? { tags: normalizeFmeTags(body.tags) } : {}),
439
+ ...(body?.owners !== undefined ? { owners: body.owners } : {}),
440
+ };
441
+ }
199
442
  return {
200
443
  name: body?.name ?? input.name,
201
444
  ...(body?.description || input.description ? { description: body?.description ?? input.description } : {}),
@@ -203,23 +446,56 @@ export const featureFlagsToolset = {
203
446
  },
204
447
  responseExtractor: passthrough,
205
448
  bodySchema: fmeFeatureFlagCreateSchema,
206
- description: "Create a feature flag in a workspace. Requires workspace_id and traffic_type_id (get from fme_traffic_type). Body requires name, optional description. Note: tags must be set via a follow-up harness_update call (Split API limitation).",
449
+ description: "Create a feature flag. Legacy mode (workspace_id): requires workspace_id + traffic_type_id (get from fme_traffic_type); body: name, optional description (no tags/owners use harness_update after creation). Harness-native mode (org_id+project_id): body requires name + trafficType (or pass traffic_type_id / traffic_type at top level), optional description/tags/owners.",
207
450
  },
208
451
  delete: {
209
452
  method: "DELETE",
210
- path: "/internal/api/v2/splits/ws/{wsId}/{featureFlagName}",
453
+ path: "",
454
+ routeResolver: (input) => {
455
+ const mode = resolveFmeDualMode(input, "fme_feature_flag");
456
+ const flagName = encodeURIComponent(requireFmeIdentifier(input, "feature_flag_name", "fme_feature_flag"));
457
+ if (mode.mode === "legacy") {
458
+ return { path: `/internal/api/v2/splits/ws/${encodeURIComponent(mode.workspaceId)}/${flagName}` };
459
+ }
460
+ return { path: `/fme/api/v4/feature-flags/${flagName}`, product: "harness", scopeParams: FME_HARNESS_NATIVE_SCOPE_PARAMS };
461
+ },
211
462
  operationPolicy: { risk: "destructive", retryPolicy: "do_not_retry" },
212
- pathParams: { workspace_id: "wsId", feature_flag_name: "featureFlagName" },
213
463
  responseExtractor: passthrough,
214
- description: "Delete a feature flag from a workspace",
464
+ description: "Delete a feature flag from a workspace (legacy) or org_id+project_id-scoped project (Harness-native)",
215
465
  },
216
466
  update: {
217
467
  method: "PATCH",
218
468
  path: "/internal/api/v2/splits/ws/{wsId}/{featureFlagName}",
469
+ routeResolver: (input) => {
470
+ const mode = resolveFmeDualMode(input, "fme_feature_flag");
471
+ const flagName = encodeURIComponent(requireFmeIdentifier(input, "feature_flag_name", "fme_feature_flag"));
472
+ if (mode.mode === "harness_native") {
473
+ return {
474
+ path: `/fme/api/v4/feature-flags/${flagName}`,
475
+ product: "harness",
476
+ scopeParams: FME_HARNESS_NATIVE_SCOPE_PARAMS,
477
+ headers: { "Content-Type": "application/merge-patch+json" },
478
+ };
479
+ }
480
+ return { path: `/internal/api/v2/splits/ws/${encodeURIComponent(mode.workspaceId)}/${flagName}` };
481
+ },
219
482
  operationPolicy: { risk: "low_write", retryPolicy: "safe" },
220
483
  pathParams: { workspace_id: "wsId", feature_flag_name: "featureFlagName" },
221
484
  bodyBuilder: (input) => {
222
485
  const body = input.body;
486
+ if (isFmeHarnessNativeSelected(input, "fme_feature_flag")) {
487
+ // JSON Merge Patch (RFC 7396): omit a field to leave it unchanged, set it to update,
488
+ // or (for description/tags/owners) set it to null/[] to clear it. rolloutStatus is
489
+ // set-only — the backend rejects an explicit null for it.
490
+ if (!body)
491
+ return {};
492
+ return {
493
+ ...(body.description !== undefined ? { description: body.description } : {}),
494
+ ...(body.tags !== undefined ? { tags: body.tags === null ? null : normalizeFmeTags(body.tags) } : {}),
495
+ ...(body.owners !== undefined ? { owners: body.owners } : {}),
496
+ ...(body.rolloutStatus !== undefined ? { rolloutStatus: body.rolloutStatus } : {}),
497
+ };
498
+ }
223
499
  if (!body)
224
500
  return [];
225
501
  const ops = [];
@@ -242,7 +518,7 @@ export const featureFlagsToolset = {
242
518
  return ops;
243
519
  },
244
520
  responseExtractor: passthrough,
245
- description: "Update a feature flag's metadata (description, tags, rolloutStatus). Uses JSON Patch (RFC 6902) format — provide fields directly and they are converted to patch operations automatically.",
521
+ description: "Update a feature flag's metadata. Legacy mode (workspace_id): description/tags/rolloutStatus via JSON Patch (RFC 6902) — provide fields directly, they're converted to patch ops automatically. Harness-native mode (org_id+project_id): description/tags/owners/rolloutStatus via JSON Merge Patch (RFC 7396) — omit a field to leave it unchanged, set it to update, or set description/tags/owners to null (or [] for tags/owners) to clear it; rolloutStatus is set-only (no null).",
246
522
  bodySchema: fmeFeatureFlagUpdateSchema,
247
523
  },
248
524
  },
@@ -250,172 +526,409 @@ export const featureFlagsToolset = {
250
526
  kill: {
251
527
  method: "PUT",
252
528
  path: "/internal/api/v2/splits/ws/{wsId}/{featureFlagName}/environments/{environmentId}/kill",
529
+ methodBuilder: (input) => (isFmeHarnessNativeSelected(input, "fme_feature_flag") ? "POST" : "PUT"),
530
+ routeResolver: (input) => {
531
+ const mode = resolveFmeDualMode(input, "fme_feature_flag");
532
+ const flagName = encodeURIComponent(requireFmeIdentifier(input, "feature_flag_name", "fme_feature_flag"));
533
+ const environmentId = encodeURIComponent(requireFmeIdentifier(input, "environment_id", "fme_feature_flag"));
534
+ if (mode.mode === "harness_native") {
535
+ return {
536
+ path: `/fme/api/v4/feature-flag-definitions/${flagName}/kill`,
537
+ product: "harness",
538
+ scopeParams: FME_HARNESS_NATIVE_SCOPE_PARAMS,
539
+ };
540
+ }
541
+ return { path: `/internal/api/v2/splits/ws/${encodeURIComponent(mode.workspaceId)}/${flagName}/environments/${environmentId}/kill` };
542
+ },
253
543
  operationPolicy: { risk: "high_write", retryPolicy: "do_not_retry" },
254
544
  pathParams: {
255
545
  workspace_id: "wsId",
256
546
  feature_flag_name: "featureFlagName",
257
547
  environment_id: "environmentId",
258
548
  },
259
- bodyBuilder: () => ({}),
260
- responseExtractor: fmeActionExtract,
261
- actionDescription: "Kill (turn off) a feature flag in a specific environment. Requires workspace_id, feature_flag_name, and environment_id.",
262
- bodySchema: {
263
- description: "No body required identifiers are in path/query params.",
264
- fields: [],
549
+ queryParams: { environment_id: "environment_id" },
550
+ bodyBuilder: (input) => {
551
+ const body = input.body ?? {};
552
+ return {
553
+ ...(body.comment !== undefined ? { comment: body.comment } : {}),
554
+ ...(body.title !== undefined ? { title: body.title } : {}),
555
+ };
265
556
  },
557
+ responseExtractor: fmeActionExtract,
558
+ actionDescription: "Kill (turn off) a feature flag in a specific environment. Requires feature_flag_name and environment_id, plus workspace_id (legacy) or org_id+project_id (Harness-native). Optional comment/title recorded with the change.",
559
+ bodySchema: fmeFeatureFlagKillRestoreReallocateSchema,
266
560
  },
267
561
  restore: {
268
562
  method: "PUT",
269
563
  path: "/internal/api/v2/splits/ws/{wsId}/{featureFlagName}/environments/{environmentId}/restore",
564
+ methodBuilder: (input) => (isFmeHarnessNativeSelected(input, "fme_feature_flag") ? "POST" : "PUT"),
565
+ routeResolver: (input) => {
566
+ const mode = resolveFmeDualMode(input, "fme_feature_flag");
567
+ const flagName = encodeURIComponent(requireFmeIdentifier(input, "feature_flag_name", "fme_feature_flag"));
568
+ const environmentId = encodeURIComponent(requireFmeIdentifier(input, "environment_id", "fme_feature_flag"));
569
+ if (mode.mode === "harness_native") {
570
+ return {
571
+ path: `/fme/api/v4/feature-flag-definitions/${flagName}/restore`,
572
+ product: "harness",
573
+ scopeParams: FME_HARNESS_NATIVE_SCOPE_PARAMS,
574
+ };
575
+ }
576
+ return { path: `/internal/api/v2/splits/ws/${encodeURIComponent(mode.workspaceId)}/${flagName}/environments/${environmentId}/restore` };
577
+ },
270
578
  operationPolicy: { risk: "high_write", retryPolicy: "do_not_retry" },
271
579
  pathParams: {
272
580
  workspace_id: "wsId",
273
581
  feature_flag_name: "featureFlagName",
274
582
  environment_id: "environmentId",
275
583
  },
276
- bodyBuilder: () => ({}),
584
+ queryParams: { environment_id: "environment_id" },
585
+ bodyBuilder: (input) => {
586
+ const body = input.body ?? {};
587
+ return {
588
+ ...(body.comment !== undefined ? { comment: body.comment } : {}),
589
+ ...(body.title !== undefined ? { title: body.title } : {}),
590
+ };
591
+ },
277
592
  responseExtractor: fmeActionExtract,
278
- actionDescription: "Restore (re-enable) a killed feature flag in a specific environment. Requires workspace_id, feature_flag_name, and environment_id.",
279
- bodySchema: {
280
- description: "No body required — identifiers are in path/query params.",
281
- fields: [],
593
+ actionDescription: "Restore (re-enable) a killed feature flag in a specific environment. Requires feature_flag_name and environment_id, plus workspace_id (legacy) or org_id+project_id (Harness-native). Optional comment/title recorded with the change.",
594
+ bodySchema: fmeFeatureFlagKillRestoreReallocateSchema,
595
+ },
596
+ reallocate: {
597
+ method: "POST",
598
+ path: "/internal/api/v2/splits/ws/{wsId}/{featureFlagName}/environments/{environmentId}/reallocate",
599
+ routeResolver: (input) => {
600
+ const mode = resolveFmeDualMode(input, "fme_feature_flag");
601
+ const flagName = encodeURIComponent(requireFmeIdentifier(input, "feature_flag_name", "fme_feature_flag"));
602
+ const environmentId = encodeURIComponent(requireFmeIdentifier(input, "environment_id", "fme_feature_flag"));
603
+ if (mode.mode === "harness_native") {
604
+ return {
605
+ path: `/fme/api/v4/feature-flag-definitions/${flagName}/reallocate`,
606
+ product: "harness",
607
+ scopeParams: FME_HARNESS_NATIVE_SCOPE_PARAMS,
608
+ };
609
+ }
610
+ return { path: `/internal/api/v2/splits/ws/${encodeURIComponent(mode.workspaceId)}/${flagName}/environments/${environmentId}/reallocate` };
611
+ },
612
+ operationPolicy: { risk: "high_write", retryPolicy: "do_not_retry" },
613
+ pathParams: {
614
+ workspace_id: "wsId",
615
+ feature_flag_name: "featureFlagName",
616
+ environment_id: "environmentId",
617
+ },
618
+ queryParams: { environment_id: "environment_id" },
619
+ bodyBuilder: (input) => {
620
+ const body = input.body ?? {};
621
+ return {
622
+ ...(body.comment !== undefined ? { comment: body.comment } : {}),
623
+ ...(body.title !== undefined ? { title: body.title } : {}),
624
+ };
282
625
  },
626
+ responseExtractor: fmeActionExtract,
627
+ actionDescription: "Reallocate traffic across treatments for a feature flag in a specific environment, without a full targeting-rules update. Requires feature_flag_name and environment_id, plus workspace_id (legacy) or org_id+project_id (Harness-native). Optional comment/title recorded with the change.",
628
+ bodySchema: fmeFeatureFlagKillRestoreReallocateSchema,
283
629
  },
284
630
  archive: {
285
631
  method: "POST",
286
632
  path: "/internal/api/v2/splits/ws/{wsId}/{featureFlagName}/archive",
633
+ routeResolver: (input) => {
634
+ const mode = resolveFmeDualMode(input, "fme_feature_flag");
635
+ const flagName = encodeURIComponent(requireFmeIdentifier(input, "feature_flag_name", "fme_feature_flag"));
636
+ if (mode.mode === "harness_native") {
637
+ return {
638
+ path: `/fme/api/v4/feature-flags/${flagName}/archive`,
639
+ product: "harness",
640
+ scopeParams: FME_HARNESS_NATIVE_SCOPE_PARAMS,
641
+ };
642
+ }
643
+ return { path: `/internal/api/v2/splits/ws/${encodeURIComponent(mode.workspaceId)}/${flagName}/archive` };
644
+ },
287
645
  operationPolicy: { risk: "high_write", retryPolicy: "do_not_retry" },
288
646
  pathParams: { workspace_id: "wsId", feature_flag_name: "featureFlagName" },
289
- bodyBuilder: () => ({}),
290
- responseExtractor: fmeActionExtract,
291
- actionDescription: "Archive a feature flag. Requires workspace_id and feature_flag_name. Subject to OPA policy checks (409 on failure).",
292
- bodySchema: {
293
- description: "No body required for this action.",
294
- fields: [],
647
+ bodyBuilder: (input) => {
648
+ const body = input.body ?? {};
649
+ return body.comment !== undefined ? { comment: body.comment } : {};
295
650
  },
651
+ responseExtractor: fmeActionExtract,
652
+ actionDescription: "Archive a feature flag. Requires feature_flag_name, plus workspace_id (legacy) or org_id+project_id (Harness-native). Subject to OPA policy checks (409 on failure). Optional comment recorded with the change.",
653
+ bodySchema: fmeFeatureFlagArchiveSchema,
296
654
  },
297
655
  unarchive: {
298
656
  method: "POST",
299
657
  path: "/internal/api/v2/splits/ws/{wsId}/{featureFlagName}/unarchive",
658
+ routeResolver: (input) => {
659
+ const mode = resolveFmeDualMode(input, "fme_feature_flag");
660
+ const flagName = encodeURIComponent(requireFmeIdentifier(input, "feature_flag_name", "fme_feature_flag"));
661
+ if (mode.mode === "harness_native") {
662
+ return {
663
+ path: `/fme/api/v4/feature-flags/${flagName}/unarchive`,
664
+ product: "harness",
665
+ scopeParams: FME_HARNESS_NATIVE_SCOPE_PARAMS,
666
+ };
667
+ }
668
+ return { path: `/internal/api/v2/splits/ws/${encodeURIComponent(mode.workspaceId)}/${flagName}/unarchive` };
669
+ },
300
670
  operationPolicy: { risk: "high_write", retryPolicy: "do_not_retry" },
301
671
  pathParams: { workspace_id: "wsId", feature_flag_name: "featureFlagName" },
302
- bodyBuilder: () => ({}),
303
- responseExtractor: fmeActionExtract,
304
- actionDescription: "Unarchive a previously archived feature flag. Requires workspace_id and feature_flag_name. Returns 409 if the flag has dependent objects.",
305
- bodySchema: {
306
- description: "No body required for this action.",
307
- fields: [],
672
+ bodyBuilder: (input) => {
673
+ const body = input.body ?? {};
674
+ return body.comment !== undefined ? { comment: body.comment } : {};
308
675
  },
676
+ responseExtractor: fmeActionExtract,
677
+ actionDescription: "Unarchive a previously archived feature flag. Requires feature_flag_name, plus workspace_id (legacy) or org_id+project_id (Harness-native). Returns 409 if the flag has dependent objects. Optional comment recorded with the change.",
678
+ bodySchema: fmeFeatureFlagArchiveSchema,
309
679
  },
310
680
  },
311
681
  },
312
682
  {
313
683
  resourceType: "fme_feature_flag_definition",
314
684
  displayName: "FME Feature Flag Definition",
315
- description: "Detailed definition of a feature flag in a specific environment, including treatments, rules, targeting, and traffic allocation. Supports create, get, and update. Create requires treatments, defaultTreatment, and defaultRule.",
685
+ description: "Detailed definition of a feature flag in a specific environment: treatments, rules, targeting, and traffic allocation. Create requires treatments, defaultTreatment, and defaultRule. Get/create/update: pass org_id+project_id (preferred) or the deprecated workspace_id. List/delete/kill/restore/reallocate: org_id+project_id only. Native list requires feature_flag_name and uses offset/limit (max 100); it does not take environment_id. Other ops require environment_id. Kill/restore/reallocate are the same actions as on fme_feature_flag; either resource works.",
316
686
  toolset: "feature-flags",
317
687
  scope: "account",
688
+ scopeOptional: true,
318
689
  identifierFields: ["workspace_id", "environment_id", "feature_flag_name"],
319
690
  product: "fme",
691
+ listFilterFields: [
692
+ { name: "feature_flag_name", description: "Feature flag name whose definitions to list. Required.", required: true },
693
+ { name: "offset", description: "Pagination offset (default 0)", type: "number" },
694
+ { name: "limit", description: "Page size (default 100, max 100)", type: "number" },
695
+ ],
320
696
  operations: {
697
+ list: {
698
+ method: "GET",
699
+ path: "",
700
+ routeResolver: (input) => resolveNativeOnlyDefinitionRoute(input, "list", { collection: true }),
701
+ operationPolicy: { risk: "read", retryPolicy: "safe" },
702
+ queryParams: { feature_flag_name: "feature_flag_name", offset: "offset", limit: "limit" },
703
+ responseExtractor: passthrough,
704
+ description: "List feature flag definitions for a flag across environments (org_id+project_id only). Requires feature_flag_name. Pagination uses offset and limit (default 100, max 100). Do not pass environment_id.",
705
+ },
321
706
  get: {
322
707
  method: "GET",
323
708
  path: "/internal/api/v2/splits/ws/{wsId}/{featureFlagName}/environments/{environmentId}",
709
+ routeResolver: (input) => {
710
+ const mode = resolveFmeDualMode(input, "fme_feature_flag_definition");
711
+ const flagName = encodeURIComponent(requireFmeIdentifier(input, "feature_flag_name", "fme_feature_flag_definition"));
712
+ const environmentId = encodeURIComponent(requireFmeIdentifier(input, "environment_id", "fme_feature_flag_definition"));
713
+ if (mode.mode === "harness_native") {
714
+ return { path: `/fme/api/v4/feature-flag-definitions/${flagName}`, product: "harness", scopeParams: FME_HARNESS_NATIVE_SCOPE_PARAMS };
715
+ }
716
+ return { path: `/internal/api/v2/splits/ws/${encodeURIComponent(mode.workspaceId)}/${flagName}/environments/${environmentId}` };
717
+ },
324
718
  operationPolicy: { risk: "read", retryPolicy: "safe" },
325
719
  pathParams: {
326
720
  workspace_id: "wsId",
327
721
  feature_flag_name: "featureFlagName",
328
722
  environment_id: "environmentId",
329
723
  },
724
+ queryParams: { environment_id: "environment_id" },
330
725
  responseExtractor: passthrough,
331
- description: "Get feature flag definition in a specific environment (treatments, rules, default rule, traffic allocation)",
726
+ description: "Get a feature flag definition in a specific environment (treatments, rules, default rule, traffic allocation). Requires feature_flag_name and environment_id. Pass org_id+project_id (preferred) or deprecated workspace_id.",
332
727
  },
333
728
  create: {
334
729
  method: "POST",
335
730
  path: "/internal/api/v2/splits/ws/{wsId}/{featureFlagName}/environments/{environmentId}",
731
+ routeResolver: (input) => {
732
+ const mode = resolveFmeDualMode(input, "fme_feature_flag_definition");
733
+ const flagName = encodeURIComponent(requireFmeIdentifier(input, "feature_flag_name", "fme_feature_flag_definition"));
734
+ const environmentId = encodeURIComponent(requireFmeIdentifier(input, "environment_id", "fme_feature_flag_definition"));
735
+ if (mode.mode === "harness_native") {
736
+ return { path: `/fme/api/v4/feature-flag-definitions/${flagName}`, product: "harness", scopeParams: FME_HARNESS_NATIVE_SCOPE_PARAMS };
737
+ }
738
+ return { path: `/internal/api/v2/splits/ws/${encodeURIComponent(mode.workspaceId)}/${flagName}/environments/${environmentId}` };
739
+ },
336
740
  operationPolicy: { risk: "low_write", retryPolicy: "do_not_retry" },
337
741
  pathParams: {
338
742
  workspace_id: "wsId",
339
743
  feature_flag_name: "featureFlagName",
340
744
  environment_id: "environmentId",
341
745
  },
746
+ queryParams: { environment_id: "environment_id" },
342
747
  bodyBuilder: (input) => input.body,
343
748
  responseExtractor: passthrough,
344
749
  bodySchema: fmeFeatureFlagDefinitionCreateSchema,
345
- description: "Create a feature flag definition in a specific environment. Requires treatments (array of treatment objects), defaultTreatment (string matching a treatment name), and defaultRule (array of bucket objects). Optional: rules, baselineTreatment, trafficAllocation, comment.",
750
+ description: "Create a feature flag definition in a specific environment. Requires treatments (array of treatment objects), defaultTreatment (string matching a treatment name), and defaultRule (array of bucket objects). Optional: rules, baselineTreatment, trafficAllocation, comment, title (Harness-native only). Requires feature_flag_name and environment_id. Pass org_id+project_id (preferred) or deprecated workspace_id.",
346
751
  },
347
752
  update: {
348
753
  method: "PUT",
754
+ methodBuilder: (input) => (isFmeHarnessNativeSelected(input, "fme_feature_flag_definition") ? "PATCH" : "PUT"),
349
755
  path: "/internal/api/v2/splits/ws/{wsId}/{featureFlagName}/environments/{environmentId}",
756
+ routeResolver: (input) => {
757
+ const mode = resolveFmeDualMode(input, "fme_feature_flag_definition");
758
+ const flagName = encodeURIComponent(requireFmeIdentifier(input, "feature_flag_name", "fme_feature_flag_definition"));
759
+ const environmentId = encodeURIComponent(requireFmeIdentifier(input, "environment_id", "fme_feature_flag_definition"));
760
+ if (mode.mode === "harness_native") {
761
+ return {
762
+ path: `/fme/api/v4/feature-flag-definitions/${flagName}`,
763
+ product: "harness",
764
+ scopeParams: FME_HARNESS_NATIVE_SCOPE_PARAMS,
765
+ headers: { "Content-Type": "application/merge-patch+json" },
766
+ };
767
+ }
768
+ return { path: `/internal/api/v2/splits/ws/${encodeURIComponent(mode.workspaceId)}/${flagName}/environments/${environmentId}` };
769
+ },
350
770
  operationPolicy: { risk: "low_write", retryPolicy: "safe" },
351
771
  pathParams: {
352
772
  workspace_id: "wsId",
353
773
  feature_flag_name: "featureFlagName",
354
774
  environment_id: "environmentId",
355
775
  },
776
+ queryParams: { environment_id: "environment_id" },
356
777
  bodyBuilder: (input) => input.body,
357
778
  responseExtractor: passthrough,
358
779
  bodySchema: fmeFeatureFlagDefinitionUpdateSchema,
359
- description: "Update a feature flag definition in a specific environment (treatments, rules, default rule, traffic allocation, baseline treatment)",
780
+ description: "Update a feature flag definition in a specific environment (treatments, rules, default rule, traffic allocation, baseline treatment). Requires feature_flag_name and environment_id. Pass org_id+project_id (preferred) or deprecated workspace_id. Native update is JSON Merge Patch: omit a field to leave it unchanged; treatments/rules/defaultRule are omit-to-keep but reject explicit null (not clearable). Legacy update is a full replace.",
781
+ },
782
+ delete: {
783
+ method: "DELETE",
784
+ path: "",
785
+ routeResolver: (input) => resolveNativeOnlyDefinitionRoute(input, "delete"),
786
+ operationPolicy: { risk: "destructive", retryPolicy: "do_not_retry" },
787
+ queryParams: { environment_id: "environment_id" },
788
+ responseExtractor: passthrough,
789
+ description: "Delete a feature flag definition in an environment (org_id+project_id only). Requires feature_flag_name and environment_id.",
790
+ },
791
+ },
792
+ executeActions: {
793
+ kill: {
794
+ method: "POST",
795
+ path: "",
796
+ routeResolver: (input) => resolveNativeOnlyDefinitionRoute(input, "kill", { suffix: "/kill" }),
797
+ operationPolicy: { risk: "high_write", retryPolicy: "do_not_retry" },
798
+ queryParams: { environment_id: "environment_id" },
799
+ bodyBuilder: (input) => {
800
+ const body = input.body ?? {};
801
+ return {
802
+ ...(body.comment !== undefined ? { comment: body.comment } : {}),
803
+ ...(body.title !== undefined ? { title: body.title } : {}),
804
+ };
805
+ },
806
+ responseExtractor: fmeActionExtract,
807
+ actionDescription: "Kill (turn off) a feature flag definition in an environment (org_id+project_id only). Requires feature_flag_name and environment_id. Optional body comment/title. Same action as fme_feature_flag kill.",
808
+ bodySchema: fmeFeatureFlagKillRestoreReallocateSchema,
809
+ },
810
+ restore: {
811
+ method: "POST",
812
+ path: "",
813
+ routeResolver: (input) => resolveNativeOnlyDefinitionRoute(input, "restore", { suffix: "/restore" }),
814
+ operationPolicy: { risk: "high_write", retryPolicy: "do_not_retry" },
815
+ queryParams: { environment_id: "environment_id" },
816
+ bodyBuilder: (input) => {
817
+ const body = input.body ?? {};
818
+ return {
819
+ ...(body.comment !== undefined ? { comment: body.comment } : {}),
820
+ ...(body.title !== undefined ? { title: body.title } : {}),
821
+ };
822
+ },
823
+ responseExtractor: fmeActionExtract,
824
+ actionDescription: "Restore a killed feature flag definition in an environment (org_id+project_id only). Requires feature_flag_name and environment_id. Optional body comment/title. Same action as fme_feature_flag restore.",
825
+ bodySchema: fmeFeatureFlagKillRestoreReallocateSchema,
826
+ },
827
+ reallocate: {
828
+ method: "POST",
829
+ path: "",
830
+ routeResolver: (input) => resolveNativeOnlyDefinitionRoute(input, "reallocate", { suffix: "/reallocate" }),
831
+ operationPolicy: { risk: "high_write", retryPolicy: "do_not_retry" },
832
+ queryParams: { environment_id: "environment_id" },
833
+ bodyBuilder: (input) => {
834
+ const body = input.body ?? {};
835
+ return {
836
+ ...(body.comment !== undefined ? { comment: body.comment } : {}),
837
+ ...(body.title !== undefined ? { title: body.title } : {}),
838
+ };
839
+ },
840
+ responseExtractor: fmeActionExtract,
841
+ actionDescription: "Reallocate traffic across treatments for a feature flag definition in an environment (org_id+project_id only). Requires feature_flag_name and environment_id. Optional body comment/title. Same action as fme_feature_flag reallocate.",
842
+ bodySchema: fmeFeatureFlagKillRestoreReallocateSchema,
360
843
  },
361
844
  },
362
845
  },
363
846
  {
364
847
  resourceType: "fme_rollout_status",
365
848
  displayName: "FME Rollout Status",
366
- description: "Rollout status definitions for a workspace (e.g. Killed, Permanent, Ramping). Use to discover valid rollout_status_id UUIDs for filtering fme_feature_flag lists. Note: this endpoint may not be available on all account types — rollout status IDs are also returned inline with fme_feature_flag list results.",
849
+ description: "Rollout status definitions (e.g. Killed, Permanent, Ramping). Dual-mode: pass org_id+project_id (preferred) or the deprecated workspace_id. Use harness_list to discover rollout_status_id UUIDs for filtering fme_feature_flag lists. Pagination uses offset/limit (max 100; harness_list size maps to limit).",
367
850
  toolset: "feature-flags",
368
851
  scope: "account",
852
+ scopeOptional: true,
369
853
  identifierFields: ["workspace_id"],
370
854
  product: "fme",
371
855
  listFilterFields: [
372
- { name: "workspace_id", description: "FME workspace ID (get from fme_workspace)", required: true },
856
+ { name: "workspace_id", description: "FME workspace ID (get from fme_workspace). Deprecated omit and pass org_id+project_id instead for Harness-native scoping." },
857
+ { name: "offset", description: "Harness-native pagination offset (default 0)", type: "number" },
858
+ { name: "limit", description: "Harness-native page size (default 100, max 100)", type: "number" },
373
859
  ],
374
860
  operations: {
375
861
  list: {
376
862
  method: "GET",
377
- path: "/internal/api/v2/rolloutStatuses/ws/{wsId}",
863
+ path: "",
864
+ routeResolver: (input) => {
865
+ const mode = resolveFmeDualMode(input, "fme_rollout_status");
866
+ if (mode.mode === "legacy") {
867
+ return { path: `/internal/api/v2/rolloutStatuses/ws/${encodeURIComponent(mode.workspaceId)}` };
868
+ }
869
+ return { path: "/fme/api/v4/rollout-statuses", product: "harness", scopeParams: FME_HARNESS_NATIVE_SCOPE_PARAMS };
870
+ },
378
871
  operationPolicy: { risk: "read", retryPolicy: "safe" },
379
- pathParams: { workspace_id: "wsId" },
380
- responseExtractor: passthrough,
381
- description: "List rollout status definitions for a workspace (Killed, Permanent, Ramping, etc.). If this returns 404, use rolloutStatus fields from fme_feature_flag list results instead.",
872
+ queryParams: { offset: "offset", size: "limit", limit: "limit" },
873
+ responseExtractor: fmeV4PaginatedListExtract,
874
+ description: "List rollout statuses. Pass org_id+project_id (preferred) or deprecated workspace_id. Optional offset/limit (harness_list size maps to limit).",
382
875
  },
383
876
  },
384
877
  },
385
878
  // ── FME Rule-Based Segments ───────────────────────────────────────────
386
879
  {
387
880
  resourceType: "fme_rule_based_segment",
388
- displayName: "FME Rule-Based Segment",
389
- description: "Rule-based segment in a workspace. Supports list, get, create (requires traffic_type_id), and delete. Create requires traffic_type_id passed via params.",
881
+ displayName: "(Deprecated) FME Rule-Based Segment",
882
+ description: "Deprecated — use fme_segment instead for new integrations. Rule-based segment in a workspace. Supports list, get, create (requires traffic_type_id), and delete via the legacy workspace_id contract only — org_id+project_id (Harness-native) is rejected on every operation here in favor of fme_segment. Create requires traffic_type_id passed via params.",
390
883
  toolset: "feature-flags",
391
884
  scope: "account",
885
+ scopeOptional: true,
392
886
  identifierFields: ["workspace_id", "segment_name"],
393
887
  product: "fme",
394
888
  listFilterFields: [
395
- { name: "workspace_id", description: "FME workspace ID (get from fme_workspace)", required: true },
889
+ { name: "workspace_id", description: "FME workspace ID (get from fme_workspace). This resource only supports the legacy workspace_id contract; use fme_segment for Harness-native org_id+project_id scoping." },
396
890
  ],
397
891
  operations: {
398
892
  list: {
399
893
  method: "GET",
400
- path: "/internal/api/v2/rule-based-segments/ws/{wsId}",
894
+ path: "",
895
+ routeResolver: (input) => {
896
+ const mode = resolveFmeDualMode(input, "fme_rule_based_segment");
897
+ if (mode.mode === "harness_native") {
898
+ throw new Error("fme_rule_based_segment.list: Harness-native (org_id/project_id) mode is not supported on this deprecated resource — use fme_segment instead.");
899
+ }
900
+ return { path: `/internal/api/v2/rule-based-segments/ws/${encodeURIComponent(mode.workspaceId)}` };
901
+ },
401
902
  operationPolicy: { risk: "read", retryPolicy: "safe" },
402
- pathParams: { workspace_id: "wsId" },
403
903
  responseExtractor: passthrough,
404
904
  description: "List all rule-based segments in a workspace",
405
905
  },
406
906
  get: {
407
907
  method: "GET",
408
- path: "/internal/api/v2/rule-based-segments/ws/{wsId}/{rbSegmentName}",
908
+ path: "",
909
+ routeResolver: (input) => {
910
+ const mode = resolveFmeDualMode(input, "fme_rule_based_segment");
911
+ if (mode.mode === "harness_native") {
912
+ throw new Error("fme_rule_based_segment.get: Harness-native (org_id/project_id) mode is not supported on this deprecated resource — use fme_segment instead.");
913
+ }
914
+ const segmentName = encodeURIComponent(requireFmeIdentifier(input, "segment_name", "fme_rule_based_segment"));
915
+ return { path: `/internal/api/v2/rule-based-segments/ws/${encodeURIComponent(mode.workspaceId)}/${segmentName}` };
916
+ },
409
917
  operationPolicy: { risk: "read", retryPolicy: "safe" },
410
- pathParams: { workspace_id: "wsId", segment_name: "rbSegmentName" },
411
918
  responseExtractor: passthrough,
412
919
  description: "Get a rule-based segment by name (workspace-level metadata)",
413
920
  },
414
921
  create: {
415
922
  method: "POST",
416
- path: "/internal/api/v2/rule-based-segments/ws/{wsId}/trafficTypes/{trafficTypeId}",
923
+ path: "",
924
+ routeResolver: (input) => {
925
+ const mode = resolveFmeDualMode(input, "fme_rule_based_segment");
926
+ if (mode.mode === "harness_native") {
927
+ throw new Error("fme_rule_based_segment.create: Harness-native (org_id/project_id) mode is not supported on this deprecated resource — use fme_segment instead (create is not yet implemented there either).");
928
+ }
929
+ return { path: `/internal/api/v2/rule-based-segments/ws/${encodeURIComponent(mode.workspaceId)}/trafficTypes/${encodeURIComponent(requireFmeIdentifier(input, "traffic_type_id", "fme_rule_based_segment"))}` };
930
+ },
417
931
  operationPolicy: { risk: "low_write", retryPolicy: "do_not_retry" },
418
- pathParams: { workspace_id: "wsId", traffic_type_id: "trafficTypeId" },
419
932
  bodyBuilder: (input) => {
420
933
  const body = input.body;
421
934
  return {
@@ -429,9 +942,16 @@ export const featureFlagsToolset = {
429
942
  },
430
943
  delete: {
431
944
  method: "DELETE",
432
- path: "/internal/api/v2/rule-based-segments/ws/{wsId}/{rbSegmentName}",
945
+ path: "",
946
+ routeResolver: (input) => {
947
+ const mode = resolveFmeDualMode(input, "fme_rule_based_segment");
948
+ if (mode.mode === "harness_native") {
949
+ throw new Error("fme_rule_based_segment.delete: Harness-native (org_id/project_id) mode is not supported on this deprecated resource — use fme_segment instead.");
950
+ }
951
+ const segmentName = encodeURIComponent(requireFmeIdentifier(input, "segment_name", "fme_rule_based_segment"));
952
+ return { path: `/internal/api/v2/rule-based-segments/ws/${encodeURIComponent(mode.workspaceId)}/${segmentName}` };
953
+ },
433
954
  operationPolicy: { risk: "destructive", retryPolicy: "do_not_retry" },
434
- pathParams: { workspace_id: "wsId", segment_name: "rbSegmentName" },
435
955
  responseExtractor: passthrough,
436
956
  description: "Delete a rule-based segment from a workspace. Environment-level configs must be removed separately.",
437
957
  },
@@ -439,20 +959,29 @@ export const featureFlagsToolset = {
439
959
  },
440
960
  {
441
961
  resourceType: "fme_rule_based_segment_definition",
442
- displayName: "FME Rule-Based Segment Definition",
443
- description: "Environment-specific definition of a rule-based segment, including targeting rules, exclusions, and matchers. Supports list (by environment), update, and enable/disable/change_request execute actions.",
962
+ displayName: "(Deprecated) FME Rule-Based Segment Definition",
963
+ description: "Deprecated — use fme_segment_definition instead for new integrations. Environment-specific definition of a rule-based segment, including targeting rules, exclusions, and matchers. Supports list (by environment), update, and enable/disable/change_request execute actions.",
444
964
  toolset: "feature-flags",
445
965
  scope: "account",
966
+ scopeOptional: true,
446
967
  identifierFields: ["workspace_id", "environment_id", "segment_name"],
447
968
  product: "fme",
448
969
  listFilterFields: [
449
- { name: "workspace_id", description: "FME workspace ID (get from fme_workspace)", required: true },
970
+ { name: "workspace_id", description: "FME workspace ID (get from fme_workspace). Deprecated omit and pass org_id+project_id instead for Harness-native scoping." },
450
971
  { name: "environment_id", description: "FME environment ID (get from fme_environment)", required: true },
451
972
  ],
452
973
  operations: {
453
974
  list: {
454
975
  method: "GET",
455
976
  path: "/internal/api/v2/rule-based-segments/ws/{wsId}/environments/{environmentId}",
977
+ routeResolver: (input) => {
978
+ const mode = resolveFmeDualMode(input, "fme_rule_based_segment_definition");
979
+ if (mode.mode === "harness_native") {
980
+ throw new Error("fme_rule_based_segment_definition.list: Harness-native (org_id/project_id) mode is not supported on this deprecated resource — use fme_segment_definition instead.");
981
+ }
982
+ const environmentId = encodeURIComponent(requireFmeIdentifier(input, "environment_id", "fme_rule_based_segment_definition"));
983
+ return { path: `/internal/api/v2/rule-based-segments/ws/${encodeURIComponent(mode.workspaceId)}/environments/${environmentId}` };
984
+ },
456
985
  operationPolicy: { risk: "read", retryPolicy: "safe" },
457
986
  pathParams: { workspace_id: "wsId", environment_id: "environmentId" },
458
987
  responseExtractor: passthrough,
@@ -461,6 +990,15 @@ export const featureFlagsToolset = {
461
990
  update: {
462
991
  method: "PUT",
463
992
  path: "/internal/api/v2/rule-based-segments/ws/{wsId}/{rbSegmentName}/environments/{environmentId}",
993
+ routeResolver: (input) => {
994
+ const mode = resolveFmeDualMode(input, "fme_rule_based_segment_definition");
995
+ if (mode.mode === "harness_native") {
996
+ throw new Error("fme_rule_based_segment_definition.update: Harness-native (org_id/project_id) mode is not supported on this deprecated resource — use fme_segment_definition instead.");
997
+ }
998
+ const segmentName = encodeURIComponent(requireFmeIdentifier(input, "segment_name", "fme_rule_based_segment_definition"));
999
+ const environmentId = encodeURIComponent(requireFmeIdentifier(input, "environment_id", "fme_rule_based_segment_definition"));
1000
+ return { path: `/internal/api/v2/rule-based-segments/ws/${encodeURIComponent(mode.workspaceId)}/${segmentName}/environments/${environmentId}` };
1001
+ },
464
1002
  operationPolicy: { risk: "low_write", retryPolicy: "safe" },
465
1003
  pathParams: { workspace_id: "wsId", segment_name: "rbSegmentName", environment_id: "environmentId" },
466
1004
  bodyBuilder: (input) => input.body,
@@ -473,6 +1011,14 @@ export const featureFlagsToolset = {
473
1011
  enable: {
474
1012
  method: "POST",
475
1013
  path: "/internal/api/v2/rule-based-segments/{environmentId}/{rbSegmentName}",
1014
+ routeResolver: (input) => {
1015
+ if (isFmeHarnessNativeSelected(input, "fme_rule_based_segment_definition.enable")) {
1016
+ throw new Error("fme_rule_based_segment_definition.enable: Harness-native (org_id/project_id) mode is not supported on this deprecated resource, and fme_segment_definition has no enable equivalent either — pass environment_id/segment_name (legacy contract) instead.");
1017
+ }
1018
+ const environmentId = encodeURIComponent(requireFmeIdentifier(input, "environment_id", "fme_rule_based_segment_definition"));
1019
+ const segmentName = encodeURIComponent(requireFmeIdentifier(input, "segment_name", "fme_rule_based_segment_definition"));
1020
+ return { path: `/internal/api/v2/rule-based-segments/${environmentId}/${segmentName}` };
1021
+ },
476
1022
  operationPolicy: { risk: "medium_write", retryPolicy: "do_not_retry" },
477
1023
  pathParams: { environment_id: "environmentId", segment_name: "rbSegmentName" },
478
1024
  bodyBuilder: () => ({}),
@@ -483,6 +1029,14 @@ export const featureFlagsToolset = {
483
1029
  disable: {
484
1030
  method: "DELETE",
485
1031
  path: "/internal/api/v2/rule-based-segments/{environmentId}/{rbSegmentName}",
1032
+ routeResolver: (input) => {
1033
+ if (isFmeHarnessNativeSelected(input, "fme_rule_based_segment_definition.disable")) {
1034
+ throw new Error("fme_rule_based_segment_definition.disable: Harness-native (org_id/project_id) mode is not supported on this deprecated resource, and fme_segment_definition has no disable equivalent either — pass environment_id/segment_name (legacy contract) instead.");
1035
+ }
1036
+ const environmentId = encodeURIComponent(requireFmeIdentifier(input, "environment_id", "fme_rule_based_segment_definition"));
1037
+ const segmentName = encodeURIComponent(requireFmeIdentifier(input, "segment_name", "fme_rule_based_segment_definition"));
1038
+ return { path: `/internal/api/v2/rule-based-segments/${environmentId}/${segmentName}` };
1039
+ },
486
1040
  operationPolicy: { risk: "medium_write", retryPolicy: "do_not_retry" },
487
1041
  pathParams: { environment_id: "environmentId", segment_name: "rbSegmentName" },
488
1042
  responseExtractor: passthrough,
@@ -495,6 +1049,14 @@ export const featureFlagsToolset = {
495
1049
  change_request: {
496
1050
  method: "POST",
497
1051
  path: "/internal/api/v2/changeRequests/ws/{wsId}/environments/{environmentId}",
1052
+ routeResolver: (input) => {
1053
+ const mode = resolveFmeDualMode(input, "fme_rule_based_segment_definition");
1054
+ if (mode.mode === "harness_native") {
1055
+ throw new Error("fme_rule_based_segment_definition.change_request: Harness-native (org_id/project_id) mode is not supported on this deprecated resource, and fme_segment_definition has no change_request equivalent either — pass workspace_id (deprecated) instead.");
1056
+ }
1057
+ const environmentId = encodeURIComponent(requireFmeIdentifier(input, "environment_id", "fme_rule_based_segment_definition"));
1058
+ return { path: `/internal/api/v2/changeRequests/ws/${encodeURIComponent(mode.workspaceId)}/environments/${environmentId}` };
1059
+ },
498
1060
  operationPolicy: { risk: "medium_write", retryPolicy: "do_not_retry" },
499
1061
  pathParams: { workspace_id: "wsId", environment_id: "environmentId" },
500
1062
  bodyBuilder: (input) => ({
@@ -514,22 +1076,32 @@ export const featureFlagsToolset = {
514
1076
  {
515
1077
  resourceType: "fme_traffic_type",
516
1078
  displayName: "FME Traffic Type",
517
- description: "Traffic type in a workspace (e.g. 'user', 'account'). List traffic types to discover traffic_type_id values needed for identity queries and flag/segment creation.",
1079
+ description: "Traffic type (e.g. 'user', 'account'). Dual-mode: pass org_id+project_id (preferred) or the deprecated workspace_id. Use harness_list to discover traffic_type_id / name values for flag and segment create. Pagination uses offset/limit (max 100; harness_list size maps to limit).",
518
1080
  toolset: "feature-flags",
519
1081
  scope: "account",
1082
+ scopeOptional: true,
520
1083
  identifierFields: ["workspace_id"],
521
1084
  product: "fme",
522
1085
  listFilterFields: [
523
- { name: "workspace_id", description: "FME workspace ID (get from fme_workspace)", required: true },
1086
+ { name: "workspace_id", description: "FME workspace ID (get from fme_workspace). Deprecated omit and pass org_id+project_id instead for Harness-native scoping." },
1087
+ { name: "offset", description: "Harness-native pagination offset (default 0)", type: "number" },
1088
+ { name: "limit", description: "Harness-native page size (default 100, max 100)", type: "number" },
524
1089
  ],
525
1090
  operations: {
526
1091
  list: {
527
1092
  method: "GET",
528
- path: "/internal/api/v2/trafficTypes/ws/{wsId}",
1093
+ path: "",
1094
+ routeResolver: (input) => {
1095
+ const mode = resolveFmeDualMode(input, "fme_traffic_type");
1096
+ if (mode.mode === "legacy") {
1097
+ return { path: `/internal/api/v2/trafficTypes/ws/${encodeURIComponent(mode.workspaceId)}` };
1098
+ }
1099
+ return { path: "/fme/api/v4/traffic-types", product: "harness", scopeParams: FME_HARNESS_NATIVE_SCOPE_PARAMS };
1100
+ },
529
1101
  operationPolicy: { risk: "read", retryPolicy: "safe" },
530
- pathParams: { workspace_id: "wsId" },
531
- responseExtractor: passthrough,
532
- description: "List traffic types for a workspace. Returns id, name, and displayAttributeId for each traffic type.",
1102
+ queryParams: { offset: "offset", size: "limit", limit: "limit" },
1103
+ responseExtractor: fmeV4PaginatedListExtract,
1104
+ description: "List traffic types. Pass org_id+project_id (preferred) or deprecated workspace_id. Optional offset/limit (harness_list size maps to limit).",
533
1105
  },
534
1106
  },
535
1107
  },
@@ -540,14 +1112,29 @@ export const featureFlagsToolset = {
540
1112
  description: "Identity (target) in an environment. Create or update identities to manage display name aliases and custom attributes. Requires traffic_type_id and environment_id. Note: the Split Admin API does not support listing or getting individual identities — use create (batch upsert) and update (PATCH single key).",
541
1113
  toolset: "feature-flags",
542
1114
  scope: "account",
1115
+ scopeOptional: true,
543
1116
  identifierFields: ["traffic_type_id", "environment_id", "key"],
544
1117
  product: "fme",
545
1118
  operations: {
546
1119
  create: {
547
1120
  method: "POST",
548
1121
  path: "/internal/api/v2/trafficTypes/{trafficTypeId}/environments/{environmentId}/identities",
1122
+ routeResolver: (input) => {
1123
+ if (isFmeHarnessNativeSelected(input, "fme_identity.create")) {
1124
+ throw new Error("fme_identity.create: Harness-native (org_id/project_id) mode not yet implemented for this operation — pass traffic_type_id/environment_id (current contract) instead.");
1125
+ }
1126
+ const trafficTypeId = encodeURIComponent(requireFmeIdentifier(input, "traffic_type_id", "fme_identity"));
1127
+ const environmentId = encodeURIComponent(requireFmeIdentifier(input, "environment_id", "fme_identity"));
1128
+ return { path: `/internal/api/v2/trafficTypes/${trafficTypeId}/environments/${environmentId}/identities` };
1129
+ },
549
1130
  operationPolicy: { risk: "low_write", retryPolicy: "safe" },
550
1131
  pathParams: { traffic_type_id: "trafficTypeId", environment_id: "environmentId" },
1132
+ paramsSchema: {
1133
+ fields: [
1134
+ { name: "org_id", required: false, description: "Optional — pass together with project_id to select the (not yet implemented) Harness-native mode instead of the current contract." },
1135
+ { name: "project_id", required: false, description: "Optional — pass together with org_id to select the (not yet implemented) Harness-native mode instead of the current contract." },
1136
+ ],
1137
+ },
551
1138
  skipScopeBodyInjection: true,
552
1139
  bodyBuilder: (input) => {
553
1140
  const body = input.body;
@@ -572,8 +1159,23 @@ export const featureFlagsToolset = {
572
1159
  update: {
573
1160
  method: "PATCH",
574
1161
  path: "/internal/api/v2/trafficTypes/{trafficTypeId}/environments/{environmentId}/identities/{key}",
1162
+ routeResolver: (input) => {
1163
+ if (isFmeHarnessNativeSelected(input, "fme_identity.update")) {
1164
+ throw new Error("fme_identity.update: Harness-native (org_id/project_id) mode not yet implemented for this operation — pass traffic_type_id/environment_id (current contract) instead.");
1165
+ }
1166
+ const trafficTypeId = encodeURIComponent(requireFmeIdentifier(input, "traffic_type_id", "fme_identity"));
1167
+ const environmentId = encodeURIComponent(requireFmeIdentifier(input, "environment_id", "fme_identity"));
1168
+ const key = encodeURIComponent(requireFmeIdentifier(input, "key", "fme_identity"));
1169
+ return { path: `/internal/api/v2/trafficTypes/${trafficTypeId}/environments/${environmentId}/identities/${key}` };
1170
+ },
575
1171
  operationPolicy: { risk: "low_write", retryPolicy: "safe" },
576
1172
  pathParams: { traffic_type_id: "trafficTypeId", environment_id: "environmentId", key: "key" },
1173
+ paramsSchema: {
1174
+ fields: [
1175
+ { name: "org_id", required: false, description: "Optional — pass together with project_id to select the (not yet implemented) Harness-native mode instead of the current contract." },
1176
+ { name: "project_id", required: false, description: "Optional — pass together with org_id to select the (not yet implemented) Harness-native mode instead of the current contract." },
1177
+ ],
1178
+ },
577
1179
  bodyBuilder: (input) => input.body,
578
1180
  responseExtractor: passthrough,
579
1181
  bodySchema: fmeIdentityUpdateSchema,
@@ -584,51 +1186,244 @@ export const featureFlagsToolset = {
584
1186
  // ── FME Standard Segments ─────────────────────────────────────────────
585
1187
  {
586
1188
  resourceType: "fme_standard_segment",
587
- displayName: "FME Standard Segment",
588
- description: "Standard (static list) segment in a workspace. List all segments to see names, descriptions, and member counts. For member management, use fme_segment_keys.",
1189
+ displayName: "(Deprecated) FME Standard Segment",
1190
+ description: "Deprecated — use fme_segment instead for new integrations. Standard (static list) segment in a workspace. List all segments to see names, descriptions, and member counts, via the legacy workspace_id contract only — org_id+project_id (Harness-native) is rejected on every operation here in favor of fme_segment. For member management, use fme_segment_keys.",
589
1191
  toolset: "feature-flags",
590
1192
  scope: "account",
1193
+ scopeOptional: true,
591
1194
  identifierFields: ["workspace_id", "segment_name"],
592
1195
  product: "fme",
593
1196
  listFilterFields: [
594
- { name: "workspace_id", description: "Workspace ID (get from fme_workspace)", required: true },
1197
+ { name: "workspace_id", description: "Workspace ID (get from fme_workspace). This resource only supports the legacy workspace_id contract; use fme_segment for Harness-native org_id+project_id scoping." },
595
1198
  ],
596
1199
  operations: {
597
1200
  list: {
598
1201
  method: "GET",
599
- path: "/internal/api/v2/segments/ws/{wsId}",
1202
+ path: "",
1203
+ routeResolver: (input) => {
1204
+ const mode = resolveFmeDualMode(input, "fme_standard_segment");
1205
+ if (mode.mode === "harness_native") {
1206
+ throw new Error("fme_standard_segment.list: Harness-native (org_id/project_id) mode is not supported on this deprecated resource — use fme_segment instead.");
1207
+ }
1208
+ return { path: `/internal/api/v2/segments/ws/${encodeURIComponent(mode.workspaceId)}` };
1209
+ },
600
1210
  operationPolicy: { risk: "read", retryPolicy: "safe" },
601
- pathParams: { workspace_id: "wsId" },
602
1211
  responseExtractor: passthrough,
603
1212
  description: "List all standard segments in a workspace. Returns segment name, description, and creation metadata.",
604
1213
  },
605
1214
  get: {
606
1215
  method: "GET",
607
- path: "/internal/api/v2/segments/ws/{wsId}/{segmentName}",
1216
+ path: "",
1217
+ routeResolver: (input) => {
1218
+ const mode = resolveFmeDualMode(input, "fme_standard_segment");
1219
+ if (mode.mode === "harness_native") {
1220
+ throw new Error("fme_standard_segment.get: Harness-native (org_id/project_id) mode is not supported on this deprecated resource — use fme_segment instead.");
1221
+ }
1222
+ const segmentName = encodeURIComponent(requireFmeIdentifier(input, "segment_name", "fme_standard_segment"));
1223
+ return { path: `/internal/api/v2/segments/ws/${encodeURIComponent(mode.workspaceId)}/${segmentName}` };
1224
+ },
608
1225
  operationPolicy: { risk: "read", retryPolicy: "safe" },
609
- pathParams: { workspace_id: "wsId", segment_name: "segmentName" },
610
1226
  responseExtractor: passthrough,
611
1227
  description: "Get a standard segment's metadata by name.",
612
1228
  },
613
1229
  },
614
1230
  },
1231
+ {
1232
+ resourceType: "fme_segment",
1233
+ displayName: "FME Segment",
1234
+ description: "FME (Harness-native, org_id+project_id scoped). Unified segment type (standard, rule-based, and large). Supports list, get, create, delete. create requires body.type (\"standard\" | \"rule_based\" | \"large\").",
1235
+ toolset: "feature-flags",
1236
+ scope: "project",
1237
+ scopeParams: FME_HARNESS_NATIVE_SCOPE_PARAMS,
1238
+ identifierFields: ["segment_name"],
1239
+ operations: {
1240
+ list: {
1241
+ method: "GET",
1242
+ path: "",
1243
+ routeResolver: (input) => {
1244
+ requireHarnessNativeSegmentScope(input, "fme_segment");
1245
+ return { path: "/fme/api/v4/segments" };
1246
+ },
1247
+ operationPolicy: { risk: "read", retryPolicy: "safe" },
1248
+ responseExtractor: passthrough,
1249
+ description: "List all segments (standard and rule-based) in project.",
1250
+ },
1251
+ get: {
1252
+ method: "GET",
1253
+ path: "",
1254
+ routeResolver: (input) => {
1255
+ requireHarnessNativeSegmentScope(input, "fme_segment");
1256
+ const segmentName = encodeURIComponent(requireFmeIdentifier(input, "segment_name", "fme_segment"));
1257
+ return { path: `/fme/api/v4/segments/${segmentName}` };
1258
+ },
1259
+ operationPolicy: { risk: "read", retryPolicy: "safe" },
1260
+ responseExtractor: passthrough,
1261
+ description: "Get single segment by name.",
1262
+ },
1263
+ delete: {
1264
+ method: "DELETE",
1265
+ path: "",
1266
+ routeResolver: (input) => {
1267
+ requireHarnessNativeSegmentScope(input, "fme_segment");
1268
+ const segmentName = encodeURIComponent(requireFmeIdentifier(input, "segment_name", "fme_segment"));
1269
+ return { path: `/fme/api/v4/segments/${segmentName}` };
1270
+ },
1271
+ operationPolicy: { risk: "destructive", retryPolicy: "do_not_retry" },
1272
+ responseExtractor: passthrough,
1273
+ description: "Delete a segment by name.",
1274
+ },
1275
+ create: {
1276
+ method: "POST",
1277
+ path: "",
1278
+ routeResolver: (input) => {
1279
+ requireHarnessNativeSegmentScope(input, "fme_segment");
1280
+ return { path: "/fme/api/v4/segments" };
1281
+ },
1282
+ operationPolicy: { risk: "low_write", retryPolicy: "do_not_retry" },
1283
+ bodyBuilder: (input) => {
1284
+ const body = input.body;
1285
+ const trafficType = (body?.trafficType ?? body?.traffic_type);
1286
+ if (body?.type !== undefined && !FME_SEGMENT_TYPES.includes(body.type)) {
1287
+ throw new Error(`fme_segment.create: invalid type '${body.type}'. Must be one of: ${FME_SEGMENT_TYPES.join(", ")}.`);
1288
+ }
1289
+ return {
1290
+ name: body?.name,
1291
+ trafficType,
1292
+ type: body?.type,
1293
+ ...(body?.description !== undefined ? { description: body.description } : {}),
1294
+ ...(body?.tags !== undefined ? { tags: normalizeFmeTags(body.tags) } : {}),
1295
+ ...(body?.owners !== undefined ? { owners: body.owners } : {}),
1296
+ };
1297
+ },
1298
+ responseExtractor: passthrough,
1299
+ bodySchema: fmeSegmentCreateSchema,
1300
+ description: "Create a new segment. Body requires name + trafficType + type (\"standard\" | \"rule_based\" | \"large\"); optional description, tags, owners.",
1301
+ },
1302
+ },
1303
+ },
1304
+ // ── FME Segment Definition (Harness-native, unified — Harness_Split/Main PR #12644) ──
1305
+ {
1306
+ resourceType: "fme_segment_definition",
1307
+ displayName: "FME Segment Definition",
1308
+ description: "Environment-specific definition of a segment (standard or rule-based) — description and lifecycle. Replaces fme_rule_based_segment_definition's role in Harness-native calls, generalized for all segment types. Harness-native only (org_id+project_id; no legacy workspace_id support). Supports list, get, create, update (description only, via JSON Merge Patch), and delete. Wired against Harness_Split/Main PR #12644, which was open (not yet merged) as of this writing — paths may still change before merge. There is no enable/disable/change_request action: the backend has no such endpoints for this unified resource; governance checks are surfaced inline in the create/update/delete responses instead.",
1309
+ toolset: "feature-flags",
1310
+ scope: "project",
1311
+ scopeParams: FME_HARNESS_NATIVE_SCOPE_PARAMS,
1312
+ identifierFields: ["segment_name", "environment_id"],
1313
+ listFilterFields: [
1314
+ { name: "environment_id", description: "FME environment ID (get from fme_environment)", required: true },
1315
+ { name: "status", description: "Filter by definition status", enum: ["ACTIVE", "ARCHIVED"] },
1316
+ { name: "offset", description: "Pagination offset", type: "number" },
1317
+ { name: "limit", description: "Page size (max 100, default 100)", type: "number" },
1318
+ ],
1319
+ operations: {
1320
+ list: {
1321
+ method: "GET",
1322
+ path: "",
1323
+ routeResolver: (input) => {
1324
+ requireHarnessNativeSegmentScope(input, "fme_segment_definition");
1325
+ return { path: "/fme/api/v4/segment-definitions" };
1326
+ },
1327
+ operationPolicy: { risk: "read", retryPolicy: "safe" },
1328
+ queryParams: { environment_id: "environment_id", status: "status", offset: "offset", limit: "limit" },
1329
+ responseExtractor: passthrough,
1330
+ description: "List segment definitions in an environment, with pagination (offset/limit, max 100) and an optional status filter.",
1331
+ },
1332
+ get: {
1333
+ method: "GET",
1334
+ path: "",
1335
+ routeResolver: (input) => {
1336
+ requireHarnessNativeSegmentScope(input, "fme_segment_definition");
1337
+ const segmentName = encodeURIComponent(requireFmeIdentifier(input, "segment_name", "fme_segment_definition"));
1338
+ return { path: `/fme/api/v4/segment-definitions/${segmentName}` };
1339
+ },
1340
+ operationPolicy: { risk: "read", retryPolicy: "safe" },
1341
+ queryParams: { environment_id: "environment_id" },
1342
+ responseExtractor: passthrough,
1343
+ description: "Get a single segment definition by name in an environment.",
1344
+ },
1345
+ create: {
1346
+ method: "POST",
1347
+ path: "",
1348
+ routeResolver: (input) => {
1349
+ requireHarnessNativeSegmentScope(input, "fme_segment_definition");
1350
+ const segmentName = encodeURIComponent(requireFmeIdentifier(input, "segment_name", "fme_segment_definition"));
1351
+ return { path: `/fme/api/v4/segment-definitions/${segmentName}` };
1352
+ },
1353
+ operationPolicy: { risk: "low_write", retryPolicy: "do_not_retry" },
1354
+ queryParams: { environment_id: "environment_id" },
1355
+ bodyBuilder: (input) => {
1356
+ const body = input.body;
1357
+ return body?.description !== undefined ? { description: body.description } : {};
1358
+ },
1359
+ responseExtractor: passthrough,
1360
+ bodySchema: fmeSegmentDefinitionCreateSchema,
1361
+ description: "Create a segment definition in an environment. Omit the description (or send an empty body) to create an empty/default shell.",
1362
+ },
1363
+ update: {
1364
+ method: "PATCH",
1365
+ path: "",
1366
+ routeResolver: (input) => {
1367
+ requireHarnessNativeSegmentScope(input, "fme_segment_definition");
1368
+ const segmentName = encodeURIComponent(requireFmeIdentifier(input, "segment_name", "fme_segment_definition"));
1369
+ return { path: `/fme/api/v4/segment-definitions/${segmentName}` };
1370
+ },
1371
+ operationPolicy: { risk: "low_write", retryPolicy: "safe" },
1372
+ queryParams: { environment_id: "environment_id" },
1373
+ headers: { "Content-Type": "application/merge-patch+json" },
1374
+ bodyBuilder: (input) => {
1375
+ const body = input.body;
1376
+ if (!body || !("description" in body))
1377
+ return {};
1378
+ return { description: body.description };
1379
+ },
1380
+ responseExtractor: passthrough,
1381
+ bodySchema: fmeSegmentDefinitionUpdateSchema,
1382
+ description: "Update a segment definition's description via JSON Merge Patch (RFC 7396) — the only mutable field. Omit description to leave it unchanged, pass null to clear it, or a string to set it.",
1383
+ },
1384
+ delete: {
1385
+ method: "DELETE",
1386
+ path: "",
1387
+ routeResolver: (input) => {
1388
+ requireHarnessNativeSegmentScope(input, "fme_segment_definition");
1389
+ const segmentName = encodeURIComponent(requireFmeIdentifier(input, "segment_name", "fme_segment_definition"));
1390
+ return { path: `/fme/api/v4/segment-definitions/${segmentName}` };
1391
+ },
1392
+ operationPolicy: { risk: "destructive", retryPolicy: "do_not_retry" },
1393
+ queryParams: { environment_id: "environment_id" },
1394
+ responseExtractor: passthrough,
1395
+ description: "Delete a segment definition from an environment.",
1396
+ },
1397
+ },
1398
+ },
615
1399
  {
616
1400
  resourceType: "fme_segment_keys",
617
1401
  displayName: "FME Segment Keys",
618
1402
  description: "Membership keys (members) of a standard segment. List keys with pagination, or update to add members. Removal is not supported by this endpoint. Limit: 10,000 keys per request, 100,000 per segment total.",
619
1403
  toolset: "feature-flags",
620
1404
  scope: "account",
1405
+ scopeOptional: true,
621
1406
  identifierFields: ["environment_id", "segment_name"],
622
1407
  product: "fme",
623
1408
  listFilterFields: [
624
1409
  { name: "environment_id", description: "Environment ID (get from fme_environment)", required: true },
625
1410
  { name: "segment_name", description: "Segment name", required: true },
626
1411
  { name: "offset", description: "Pagination offset", type: "number" },
1412
+ { name: "org_id", description: "Optional — pass together with project_id to select the (not yet implemented) Harness-native mode instead of the current contract." },
1413
+ { name: "project_id", description: "Optional — pass together with org_id to select the (not yet implemented) Harness-native mode instead of the current contract." },
627
1414
  ],
628
1415
  operations: {
629
1416
  list: {
630
1417
  method: "GET",
631
1418
  path: "/internal/api/v2/segments/{environmentId}/{segmentName}/keys",
1419
+ routeResolver: (input) => {
1420
+ if (isFmeHarnessNativeSelected(input, "fme_segment_keys.list")) {
1421
+ throw new Error("fme_segment_keys.list: Harness-native (org_id/project_id) mode not yet implemented for this operation — pass environment_id/segment_name (current contract) instead.");
1422
+ }
1423
+ const environmentId = encodeURIComponent(requireFmeIdentifier(input, "environment_id", "fme_segment_keys"));
1424
+ const segmentName = encodeURIComponent(requireFmeIdentifier(input, "segment_name", "fme_segment_keys"));
1425
+ return { path: `/internal/api/v2/segments/${environmentId}/${segmentName}/keys` };
1426
+ },
632
1427
  operationPolicy: { risk: "read", retryPolicy: "safe" },
633
1428
  pathParams: { environment_id: "environmentId", segment_name: "segmentName" },
634
1429
  queryParams: {
@@ -641,6 +1436,14 @@ export const featureFlagsToolset = {
641
1436
  update: {
642
1437
  method: "PUT",
643
1438
  path: "/internal/api/v2/segments/{environmentId}/{segmentName}/upload",
1439
+ routeResolver: (input) => {
1440
+ if (isFmeHarnessNativeSelected(input, "fme_segment_keys.update")) {
1441
+ throw new Error("fme_segment_keys.update: Harness-native (org_id/project_id) mode not yet implemented for this operation — pass environment_id/segment_name (current contract) instead.");
1442
+ }
1443
+ const environmentId = encodeURIComponent(requireFmeIdentifier(input, "environment_id", "fme_segment_keys"));
1444
+ const segmentName = encodeURIComponent(requireFmeIdentifier(input, "segment_name", "fme_segment_keys"));
1445
+ return { path: `/internal/api/v2/segments/${environmentId}/${segmentName}/upload` };
1446
+ },
644
1447
  operationPolicy: { risk: "medium_write", retryPolicy: "do_not_retry" },
645
1448
  pathParams: { environment_id: "environmentId", segment_name: "segmentName" },
646
1449
  skipScopeBodyInjection: true,