@growthbook/mcp 1.7.0 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@growthbook/mcp",
3
3
  "mcpName": "io.github.growthbook/growthbook-mcp",
4
- "version": "1.7.0",
4
+ "version": "1.8.1",
5
5
  "description": "MCP Server for interacting with GrowthBook",
6
6
  "access": "public",
7
7
  "homepage": "https://github.com/growthbook/growthbook-mcp",
@@ -38,12 +38,12 @@
38
38
  "author": "GrowthBook",
39
39
  "license": "MIT",
40
40
  "dependencies": {
41
- "@modelcontextprotocol/sdk": "^1.25.3",
41
+ "@modelcontextprotocol/sdk": "^1.27.1",
42
42
  "env-paths": "^4.0.0",
43
43
  "zod": "^4.3.6"
44
44
  },
45
45
  "devDependencies": {
46
- "@types/node": "^25.1.0",
46
+ "@types/node": "^25.3.5",
47
47
  "@vitest/coverage-v8": "^4.0.18",
48
48
  "typescript": "^5.9.2",
49
49
  "vitest": "^4.0.18"
@@ -425,25 +425,6 @@ export function formatDefaults(defaults) {
425
425
  return parts.join("\n");
426
426
  }
427
427
  // ─── Stale Features ─────────────────────────────────────────────────
428
- // Common SDK patterns to search for when removing a flag from the codebase
429
- const SDK_PATTERNS = [
430
- // JS/TS/React
431
- "isOn",
432
- "getFeatureValue",
433
- "useFeatureIsOn",
434
- "useFeatureValue",
435
- "evalFeature",
436
- // Python
437
- "is_on",
438
- "get_feature_value",
439
- // Go / Ruby / other
440
- "IsOn",
441
- "GetFeatureValue",
442
- "feature_is_on",
443
- ];
444
- function buildSearchPatterns(flagId) {
445
- return SDK_PATTERNS.map((fn) => `${fn}("${flagId}")`).join(", ");
446
- }
447
428
  export function formatStaleFeatureFlags(data, requestedIds) {
448
429
  const features = data.features || {};
449
430
  const foundIds = Object.keys(features);
@@ -509,7 +490,7 @@ export function formatStaleFeatureFlags(data, requestedIds) {
509
490
  parts.push(`- **\`${f.featureId}\`**: STALE (${f.staleReason}) — needs manual review`);
510
491
  }
511
492
  parts.push(` ${envNote}`);
512
- parts.push(` Search for: ${buildSearchPatterns(id)}`);
493
+ parts.push(` Search for \`${id}\` in relevant source files to find usages.`);
513
494
  parts.push("");
514
495
  }
515
496
  // Summary
@@ -242,12 +242,16 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
242
242
  confirmedDefaultsReviewed: z
243
243
  .boolean()
244
244
  .describe("Set to true to confirm you have called get_defaults and reviewed the output to guide these parameters."),
245
+ customFields: z
246
+ .record(z.string(), z.string())
247
+ .optional()
248
+ .describe("Custom field values as key-value pairs. Keys are custom field IDs, values are string representations (e.g. {\"priority\": \"high\", \"team\": \"growth\"})."),
245
249
  }),
246
250
  annotations: {
247
251
  readOnlyHint: false,
248
252
  destructiveHint: false,
249
253
  },
250
- }, async ({ description, hypothesis, name, valueType, variations, fileExtension, confirmedDefaultsReviewed, project, featureId, }) => {
254
+ }, async ({ description, hypothesis, name, valueType, variations, fileExtension, confirmedDefaultsReviewed, project, featureId, customFields, }) => {
251
255
  if (!confirmedDefaultsReviewed) {
252
256
  return {
253
257
  content: [
@@ -275,6 +279,7 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
275
279
  name: variation.name,
276
280
  })),
277
281
  ...(project && { project }),
282
+ ...(customFields && { customFields }),
278
283
  };
279
284
  try {
280
285
  const experimentRes = await fetchWithRateLimit(`${baseApiUrl}/api/v1/experiments`, {
@@ -15,7 +15,7 @@ export function registerFeatureTools({ server, baseApiUrl, apiKey, appOrigin, us
15
15
  readOnlyHint: false,
16
16
  destructiveHint: false,
17
17
  },
18
- }, async ({ id, valueType, defaultValue, description, project, fileExtension, }) => {
18
+ }, async ({ id, valueType, defaultValue, description, project, fileExtension, customFields, }) => {
19
19
  // get environments
20
20
  let environments = [];
21
21
  const defaults = await getDefaults(apiKey, baseApiUrl);
@@ -45,6 +45,7 @@ export function registerFeatureTools({ server, baseApiUrl, apiKey, appOrigin, us
45
45
  return acc;
46
46
  }, {}),
47
47
  ...(project && { project }),
48
+ ...(customFields && { customFields }),
48
49
  };
49
50
  try {
50
51
  const res = await fetchWithRateLimit(`${baseApiUrl}/api/v1/features`, {
@@ -139,7 +140,7 @@ export function registerFeatureTools({ server, baseApiUrl, apiKey, appOrigin, us
139
140
  */
140
141
  server.registerTool("get_feature_flags", {
141
142
  title: "Get Feature Flags",
142
- description: "Lists feature flags in your GrowthBook organization, or fetches details for a specific flag by ID. Use to find existing flags before creating new ones, get a flag's current configuration and rules, or find flag IDs needed for create_force_rule or create_experiment. Single flag fetch (via featureFlagId) returns full config including environment rules. If flag is archived, suggest removing from codebase.",
143
+ description: "Lists feature flags with full details (rules, environments, values) or fetches a single flag by ID. Returns up to 100 flags per page. Use to inspect flag configuration, rules, and status. For a lightweight list of all flag IDs (no limit), use list_feature_keys instead.",
143
144
  inputSchema: z.object({
144
145
  project: featureFlagSchema.project.optional(),
145
146
  featureFlagId: featureFlagSchema.id.optional(),
@@ -187,17 +188,57 @@ export function registerFeatureTools({ server, baseApiUrl, apiKey, appOrigin, us
187
188
  ]));
188
189
  }
189
190
  });
191
+ /**
192
+ * Tool: list_feature_keys
193
+ */
194
+ server.registerTool("list_feature_keys", {
195
+ title: "List Feature Keys",
196
+ description: "Returns all feature flag IDs (keys only, no details) in your GrowthBook organization. Useful for discovering flag IDs when you need to check a large number of flags — for example, before calling get_stale_feature_flags. Optionally filter by project.",
197
+ inputSchema: z.object({
198
+ projectId: z
199
+ .string()
200
+ .optional()
201
+ .describe("Filter by project ID to only return flags in that project."),
202
+ }),
203
+ annotations: {
204
+ readOnlyHint: true,
205
+ },
206
+ }, async ({ projectId }) => {
207
+ try {
208
+ const queryParams = projectId
209
+ ? `?projectId=${encodeURIComponent(projectId)}`
210
+ : "";
211
+ const res = await fetchWithRateLimit(`${baseApiUrl}/api/v1/feature-keys${queryParams}`, {
212
+ headers: buildHeaders(apiKey),
213
+ });
214
+ await handleResNotOk(res);
215
+ const keys = (await res.json());
216
+ return {
217
+ content: [
218
+ {
219
+ type: "text",
220
+ text: `**${keys.length} feature flag(s) found${projectId ? ` in project \`${projectId}\`` : ""}:**\n\n${keys.map((k) => `\`${k}\``).join(", ")}`,
221
+ },
222
+ ],
223
+ };
224
+ }
225
+ catch (error) {
226
+ throw new Error(formatApiError(error, "fetching feature keys", [
227
+ "Check that your GB_API_KEY has permission to read features.",
228
+ ]));
229
+ }
230
+ });
190
231
  /**
191
232
  * Tool: get_stale_feature_flags
192
233
  */
193
234
  server.registerTool("get_stale_feature_flags", {
194
235
  title: "Get Stale Feature Flags",
195
- description: "Given a list of feature flag IDs, checks whether each one is stale and returns cleanup guidance including replacement values and SDK search patterns. You MUST provide featureIds — gather them first from the user, from the current file context, or by grepping the codebase for SDK patterns (isOn, getFeatureValue, useFeatureIsOn, useFeatureValue, evalFeature).",
236
+ description: "Given a list of feature flag IDs, checks whether each one is stale and returns cleanup guidance including replacement values and SDK search patterns. You MUST provide featureIds — gather them first from the user, from the current file context, or by using list_feature_keys to get all flag IDs and then searching the codebase for those IDs to determine which are present.",
196
237
  inputSchema: z.object({
197
238
  featureIds: z
198
239
  .array(z.string())
199
240
  .optional()
200
- .describe("REQUIRED. One or more feature flag IDs to check (e.g. [\"my-feature\", \"dark-mode\"]). Gather IDs first from the user, from code context, or by grepping for SDK usage patterns."),
241
+ .describe("REQUIRED. One or more feature flag IDs to check (e.g. [\"my-feature\", \"dark-mode\"]). Gather IDs first from the user, from code context, or by using list_feature_keys to get all flag IDs and searching the codebase for those IDs."),
201
242
  }),
202
243
  annotations: {
203
244
  readOnlyHint: true,
@@ -215,8 +256,7 @@ export function registerFeatureTools({ server, baseApiUrl, apiKey, appOrigin, us
215
256
  "To gather feature flag IDs, try one of these approaches:",
216
257
  "1. **Ask the user** which flags they want to check",
217
258
  "2. **Extract from current file context** — look for flag IDs in the open file",
218
- "3. **Grep the codebase** for GrowthBook SDK patterns:",
219
- ' `grep -rn "isOn\\|getFeatureValue\\|useFeatureIsOn\\|useFeatureValue\\|evalFeature" --include="*.{ts,tsx,js,jsx,py,go,rb}"`',
259
+ "3. **Use the `list_feature_keys` tool** to get all flag IDs, then search the codebase for those IDs to determine which are present",
220
260
  "",
221
261
  "Then call this tool again with the discovered flag IDs.",
222
262
  ].join("\n"),
package/server/utils.js CHANGED
@@ -405,6 +405,10 @@ export const featureFlagSchema = {
405
405
  fileExtension: z
406
406
  .enum(SUPPORTED_FILE_EXTENSIONS)
407
407
  .describe("The extension of the current file. If it's unclear, ask the user."),
408
+ customFields: z
409
+ .record(z.string(), z.string())
410
+ .optional()
411
+ .describe("Custom field values as key-value pairs. Keys are custom field IDs, values are string representations (e.g. {\"priority\": \"high\", \"team\": \"growth\"})."),
408
412
  };
409
413
  function sleep(ms) {
410
414
  return new Promise((resolve) => setTimeout(resolve, ms));