@growthbook/mcp 0.1.2 → 1.0.0

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/README.md CHANGED
@@ -14,7 +14,7 @@ Use the following env variables to configure the MCP server.
14
14
  | Variable Name | Status | Description |
15
15
  | ------------- | -------- | ----------------------------------------------------------------- |
16
16
  | GB_API_KEY | Required | A GrowthBook API key. |
17
- | GB_USER | Required | Your name. Used when creating a feature flag. |
17
+ | GB_EMAIL | Required | Your email address used with GrowthBook. Used when creating feature flags and experiments.|
18
18
  | GB_API_URL | Optional | Your GrowthBook API URL. Defaults to `https://api.growthbook.io`. |
19
19
  | GB_APP_ORIGIN | Optional | Your GrowthBook app URL Defaults to `https://app.growthbook.io`. |
20
20
 
@@ -36,7 +36,7 @@ Find instructions below to add the MCP server to a client. Any client that suppo
36
36
  "GB_API_KEY": "YOUR_API_KEY",
37
37
  "GB_API_URL": "YOUR_API_URL",
38
38
  "GB_APP_ORIGIN": "YOUR_APP_ORIGIN",
39
- "GB_USER": "YOUR_NAME"
39
+ "GB_EMAIL": "YOUR_EMAIL"
40
40
  }
41
41
  }
42
42
  }
@@ -64,7 +64,7 @@ You should now see a green active status after the server successfully connects!
64
64
  "GB_API_KEY": "YOUR_API_KEY",
65
65
  "GB_API_URL": "YOUR_API_URL",
66
66
  "GB_APP_ORIGIN": "YOUR_APP_ORIGIN",
67
- "GB_USER": "YOUR_NAME"
67
+ "GB_EMAIL": "YOUR_EMAIL"
68
68
  }
69
69
  }
70
70
  }
@@ -92,7 +92,7 @@ GrowthBook MCP is now ready to use in VS Code.
92
92
  "GB_API_KEY": "YOUR_API_KEY",
93
93
  "GB_API_URL": "YOUR_API_URL",
94
94
  "GB_APP_ORIGIN": "YOUR_APP_ORIGIN",
95
- "GB_USER": "YOUR_NAME"
95
+ "GB_EMAIL": "YOUR_EMAIL"
96
96
  }
97
97
  }
98
98
  }
@@ -122,6 +122,9 @@ A hammer icon should appear in the chat window, indicating that your GrowthBook
122
122
  - `get_experiment`: Fetch details for a specific experiment by ID.
123
123
  - `get_attributes`: List all user attributes tracked in GrowthBook (useful for targeting).
124
124
  - `create_experiment`: Creates a feature-flag based experiment.
125
+ - `get_defaults`: Get default values for experiments including hypothesis, description, datasource, and assignment query. (Runs automatically when the create experiment tool is called.)
126
+ - `create_defaults`: Set custom default values for experiments that will be used when creating new experiments.
127
+ - `clear_user_defaults`: Clear user-defined defaults and revert to automatic defaults.
125
128
 
126
129
  - **Environments**
127
130
 
package/build/index.js CHANGED
@@ -12,7 +12,7 @@ import { registerDefaultsTools } from "./tools/defaults.js";
12
12
  export const baseApiUrl = getApiUrl();
13
13
  export const apiKey = getApiKey();
14
14
  export const appOrigin = getAppOrigin();
15
- export const user = getUser();
15
+ export const user = await getUser(baseApiUrl, apiKey);
16
16
  // Create an MCP server
17
17
  const server = new McpServer({
18
18
  name: "GrowthBook MCP",
@@ -1,10 +1,12 @@
1
1
  import { handleResNotOk } from "../utils.js";
2
2
  import envPaths from "env-paths";
3
- import { writeFile, readFile } from "fs/promises";
3
+ import { writeFile, readFile, mkdir, unlink } from "fs/promises";
4
4
  import { join } from "path";
5
+ import { z } from "zod";
5
6
  const paths = envPaths("growthbook-mcp"); // Use your app name
6
7
  const experimentDefaultsDir = paths.config; // This is the recommended config directory
7
8
  const experimentDefaultsFile = join(experimentDefaultsDir, "experiment-defaults.json");
9
+ const userDefaultsFile = join(experimentDefaultsDir, "user-defaults.json");
8
10
  export async function createDefaults(apiKey, baseApiUrl) {
9
11
  try {
10
12
  const experimentsResponse = await fetch(`${baseApiUrl}/api/v1/experiments`, {
@@ -42,6 +44,10 @@ export async function createDefaults(apiKey, baseApiUrl) {
42
44
  datasource: "",
43
45
  assignmentQuery,
44
46
  environments,
47
+ filePaths: {
48
+ experimentDefaultsFile,
49
+ userDefaultsFile,
50
+ },
45
51
  timestamp: new Date().toISOString(),
46
52
  };
47
53
  }
@@ -121,6 +127,10 @@ export async function createDefaults(apiKey, baseApiUrl) {
121
127
  datasource: mostFrequentDS.ds,
122
128
  assignmentQuery: mostFrequentDS.aq,
123
129
  environments,
130
+ filePaths: {
131
+ experimentDefaultsFile,
132
+ userDefaultsFile,
133
+ },
124
134
  timestamp: new Date().toISOString(),
125
135
  };
126
136
  }
@@ -128,16 +138,94 @@ export async function createDefaults(apiKey, baseApiUrl) {
128
138
  throw error;
129
139
  }
130
140
  }
141
+ async function getUserDefaults() {
142
+ try {
143
+ const userDefaultsData = await readFile(userDefaultsFile, "utf8");
144
+ return JSON.parse(userDefaultsData);
145
+ }
146
+ catch (error) {
147
+ if (error.code === "ENOENT") {
148
+ return null;
149
+ }
150
+ throw error;
151
+ }
152
+ }
131
153
  export async function getDefaults(apiKey, baseApiUrl) {
154
+ // First check for user-defined defaults
155
+ const userDefaults = await getUserDefaults();
156
+ if (userDefaults) {
157
+ // User has set defaults, use them for datasource/assignment/environments
158
+ // But still get the automatic defaults for names/hypotheses/descriptions
159
+ let autoDefaults = {
160
+ name: [],
161
+ hypothesis: [],
162
+ description: [],
163
+ };
164
+ try {
165
+ // Try to get existing auto-generated defaults for name/hypothesis/description
166
+ const experimentDefaultsData = await readFile(experimentDefaultsFile, "utf8");
167
+ const parsedExperimentDefaults = JSON.parse(experimentDefaultsData);
168
+ if (parsedExperimentDefaults &&
169
+ new Date(parsedExperimentDefaults.timestamp).getTime() >
170
+ new Date().getTime() - 1000 * 60 * 60 * 24 * 30 // 30 days
171
+ ) {
172
+ autoDefaults = {
173
+ name: parsedExperimentDefaults.name || [],
174
+ hypothesis: parsedExperimentDefaults.hypothesis || [],
175
+ description: parsedExperimentDefaults.description || [],
176
+ };
177
+ }
178
+ else {
179
+ // Re-generate auto defaults if expired
180
+ const generatedDefaults = await createDefaults(apiKey, baseApiUrl);
181
+ await mkdir(experimentDefaultsDir, { recursive: true });
182
+ await writeFile(experimentDefaultsFile, JSON.stringify(generatedDefaults, null, 2));
183
+ autoDefaults = {
184
+ name: generatedDefaults.name,
185
+ hypothesis: generatedDefaults.hypothesis,
186
+ description: generatedDefaults.description,
187
+ };
188
+ }
189
+ }
190
+ catch (error) {
191
+ if (error.code === "ENOENT") {
192
+ // Generate new auto defaults
193
+ const generatedDefaults = await createDefaults(apiKey, baseApiUrl);
194
+ await mkdir(experimentDefaultsDir, { recursive: true });
195
+ await writeFile(experimentDefaultsFile, JSON.stringify(generatedDefaults, null, 2));
196
+ autoDefaults = {
197
+ name: generatedDefaults.name,
198
+ hypothesis: generatedDefaults.hypothesis,
199
+ description: generatedDefaults.description,
200
+ };
201
+ }
202
+ }
203
+ // Combine user defaults with auto defaults
204
+ return {
205
+ name: autoDefaults.name || [],
206
+ hypothesis: autoDefaults.hypothesis || [],
207
+ description: autoDefaults.description || [],
208
+ datasource: userDefaults.datasourceId,
209
+ assignmentQuery: userDefaults.assignmentQueryId,
210
+ environments: userDefaults.environments,
211
+ filePaths: {
212
+ experimentDefaultsFile,
213
+ userDefaultsFile,
214
+ },
215
+ timestamp: new Date().toISOString(),
216
+ };
217
+ }
218
+ // No user defaults, use fully automatic defaults
132
219
  let experimentDefaults;
133
220
  try {
134
221
  const experimentDefaultsData = await readFile(experimentDefaultsFile, "utf8");
135
222
  let parsedExperimentDefaults = JSON.parse(experimentDefaultsData);
136
223
  if (!parsedExperimentDefaults ||
137
- parsedExperimentDefaults.timestamp <
224
+ new Date(parsedExperimentDefaults.timestamp).getTime() <
138
225
  new Date().getTime() - 1000 * 60 * 60 * 24 * 30 // 30 days
139
226
  ) {
140
227
  const generatedExperimentDefaults = await createDefaults(apiKey, baseApiUrl);
228
+ await mkdir(experimentDefaultsDir, { recursive: true });
141
229
  await writeFile(experimentDefaultsFile, JSON.stringify(generatedExperimentDefaults, null, 2));
142
230
  parsedExperimentDefaults = generatedExperimentDefaults;
143
231
  }
@@ -147,6 +235,7 @@ export async function getDefaults(apiKey, baseApiUrl) {
147
235
  if (error.code === "ENOENT") {
148
236
  // experimentDefaultsFile does not exist, generate new defaults
149
237
  const generatedExperimentDefaults = await createDefaults(apiKey, baseApiUrl);
238
+ await mkdir(experimentDefaultsDir, { recursive: true });
150
239
  await writeFile(experimentDefaultsFile, JSON.stringify(generatedExperimentDefaults, null, 2));
151
240
  experimentDefaults = generatedExperimentDefaults;
152
241
  }
@@ -163,7 +252,70 @@ export async function registerDefaultsTools({ server, baseApiUrl, apiKey, }) {
163
252
  server.tool("get_defaults", "Get the default values for experiments, including hypothesis, description, datasource, assignment query, and environments.", {}, async () => {
164
253
  const defaults = await getDefaults(apiKey, baseApiUrl);
165
254
  return {
166
- content: [{ type: "text", text: JSON.stringify(defaults, null, 2) }],
255
+ content: [
256
+ {
257
+ type: "text",
258
+ text: JSON.stringify(defaults, null, 2),
259
+ },
260
+ ],
167
261
  };
168
262
  });
263
+ server.tool("set_user_defaults", "Set user-defined defaults for datasource, assignment query, and environments. These will override the automatic defaults for these fields.", {
264
+ datasourceId: z.string().describe("The data source ID to use as default"),
265
+ assignmentQueryId: z
266
+ .string()
267
+ .describe("The assignment query ID to use as default"),
268
+ environments: z
269
+ .array(z.string())
270
+ .describe("List of environment IDs to use as defaults"),
271
+ }, async ({ datasourceId, assignmentQueryId, environments }) => {
272
+ try {
273
+ const userDefaults = {
274
+ datasourceId,
275
+ assignmentQueryId,
276
+ environments,
277
+ timestamp: new Date().toISOString(),
278
+ };
279
+ await mkdir(experimentDefaultsDir, { recursive: true });
280
+ await writeFile(userDefaultsFile, JSON.stringify(userDefaults, null, 2));
281
+ return {
282
+ content: [
283
+ {
284
+ type: "text",
285
+ text: `User defaults have been saved:\n\n${JSON.stringify(userDefaults, null, 2)} to ${userDefaultsFile}\n\nThese will be used when creating new experiments.`,
286
+ },
287
+ ],
288
+ };
289
+ }
290
+ catch (error) {
291
+ throw new Error(`Error setting user defaults: ${error}`);
292
+ }
293
+ });
294
+ server.tool("clear_user_defaults", "Clear user-defined defaults and revert to automatic defaults.", {}, async () => {
295
+ try {
296
+ await readFile(userDefaultsFile, "utf8");
297
+ await unlink(userDefaultsFile);
298
+ return {
299
+ content: [
300
+ {
301
+ type: "text",
302
+ text: "User defaults have been cleared. The system will now use automatic defaults.",
303
+ },
304
+ ],
305
+ };
306
+ }
307
+ catch (error) {
308
+ if (error.code === "ENOENT") {
309
+ return {
310
+ content: [
311
+ {
312
+ type: "text",
313
+ text: "No user defaults were set.",
314
+ },
315
+ ],
316
+ };
317
+ }
318
+ throw new Error(`Error clearing user defaults: ${error}`);
319
+ }
320
+ });
169
321
  }
@@ -57,7 +57,7 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
57
57
  // Loop through the environments and create a rule for each one keyed by environment name
58
58
  environments: defaultEnvironments.reduce((acc, env) => {
59
59
  acc[env] = {
60
- enabled: true,
60
+ enabled: false,
61
61
  rules: [
62
62
  {
63
63
  type: "force",
@@ -176,7 +176,12 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
176
176
  .string()
177
177
  .describe("Variation name. Base name off the examples from get_defaults. If none are available, use a short, descriptive name that captures the essence of the variation."),
178
178
  value: z
179
- .union([z.string(), z.number(), z.boolean(), z.record(z.any())])
179
+ .union([
180
+ z.string(),
181
+ z.number(),
182
+ z.boolean(),
183
+ z.record(z.string(), z.any()),
184
+ ])
180
185
  .describe("The value of the control and each of the variations. The value should be a string, number, boolean, or object. If it's an object, it should be a valid JSON object."),
181
186
  }))
182
187
  .describe("Experiment variations. The key should be the variation name and the value should be the variation value. Look to variations included in preview experiments for guidance on generation. The default or control variation should always be first."),
@@ -203,6 +208,7 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
203
208
  name,
204
209
  description,
205
210
  hypothesis,
211
+ owner: user.email,
206
212
  trackingKey: name.toLowerCase().replace(/[^a-z0-9]/g, "-"),
207
213
  tags: ["mcp"],
208
214
  assignmentQueryId: experimentDefaults?.assignmentQuery,
@@ -226,7 +232,7 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
226
232
  const flagId = `flag_${name.toLowerCase().replace(/[^a-z0-9]/g, "_")}`;
227
233
  const flagPayload = {
228
234
  id: flagId,
229
- owner: user,
235
+ owner: user.name,
230
236
  defaultValue: variations[0].value,
231
237
  valueType: typeof variations[0].value === "string"
232
238
  ? "string"
@@ -237,7 +243,7 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
237
243
  environments: {
238
244
  ...experimentDefaults.environments.reduce((acc, env) => {
239
245
  acc[env] = {
240
- enabled: true,
246
+ enabled: false,
241
247
  rules: [
242
248
  {
243
249
  type: "experiment-ref",
@@ -261,8 +267,8 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
261
267
  },
262
268
  body: JSON.stringify(flagPayload),
263
269
  });
264
- const flagData = await flagRes.json();
265
270
  await handleResNotOk(flagRes);
271
+ const flagData = await flagRes.json();
266
272
  const experimentLink = generateLinkToGrowthBook(appOrigin, "experiment", experimentData.experiment.id);
267
273
  const flagLink = generateLinkToGrowthBook(appOrigin, "features", flagData.feature.id);
268
274
  const { stub, docs, language } = getDocsMetadata(fileExtension);
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { getDocsMetadata, handleResNotOk, generateLinkToGrowthBook, SUPPORTED_FILE_EXTENSIONS, } from "../utils.js";
3
3
  import { exec } from "child_process";
4
+ import { getDefaults } from "./defaults.js";
4
5
  export function registerFeatureTools({ server, baseApiUrl, apiKey, appOrigin, user, }) {
5
6
  /**
6
7
  * Tool: create_feature_flag
@@ -14,7 +15,7 @@ export function registerFeatureTools({ server, baseApiUrl, apiKey, appOrigin, us
14
15
  .string()
15
16
  .optional()
16
17
  .default("")
17
- .describe("A briefdescription of the feature flag"),
18
+ .describe("A brief description of the feature flag"),
18
19
  valueType: z
19
20
  .enum(["string", "number", "boolean", "json"])
20
21
  .describe("The value type the feature flag will return"),
@@ -25,13 +26,37 @@ export function registerFeatureTools({ server, baseApiUrl, apiKey, appOrigin, us
25
26
  .enum(SUPPORTED_FILE_EXTENSIONS)
26
27
  .describe("The extension of the current file. If it's unclear, ask the user."),
27
28
  }, async ({ id, description, valueType, defaultValue, fileExtension }) => {
29
+ // get environments
30
+ let environments = [];
31
+ const defaults = await getDefaults(apiKey, baseApiUrl);
32
+ if (defaults.environments) {
33
+ environments = defaults.environments;
34
+ }
35
+ else {
36
+ const envRes = await fetch(`${baseApiUrl}/api/v1/features/environments`, {
37
+ headers: {
38
+ Authorization: `Bearer ${apiKey}`,
39
+ "Content-Type": "application/json",
40
+ },
41
+ });
42
+ await handleResNotOk(envRes);
43
+ const envData = await envRes.json();
44
+ environments = envData.environments.map((env) => env.id);
45
+ }
28
46
  const payload = {
29
47
  id,
30
48
  description,
31
- owner: user,
49
+ owner: user.name,
32
50
  valueType,
33
51
  defaultValue,
34
52
  tags: ["mcp"],
53
+ environments: environments.reduce((acc, env) => {
54
+ acc[env] = {
55
+ enabled: false,
56
+ rules: [],
57
+ };
58
+ return acc;
59
+ }, {}),
35
60
  };
36
61
  try {
37
62
  const res = await fetch(`${baseApiUrl}/api/v1/features`, {
package/build/utils.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { z } from "zod";
1
2
  import { getFeatureFlagDocs } from "./docs.js";
2
3
  // Shared file extension enum for all MCP tools
3
4
  export const SUPPORTED_FILE_EXTENSIONS = [
@@ -52,12 +53,39 @@ export function getAppOrigin() {
52
53
  const userAppOrigin = process.env.GB_APP_ORIGIN;
53
54
  return `${userAppOrigin || defaultAppOrigin}`;
54
55
  }
55
- export function getUser() {
56
- const user = process.env.GB_USER;
56
+ export async function getUser(baseApiUrl, apiKey) {
57
+ const user = process.env.GB_EMAIL || process.env.GB_USER;
57
58
  if (!user) {
58
- throw new Error("GB_USER environment variable is required");
59
+ throw new Error("GB_EMAIL environment variable is required");
60
+ }
61
+ // Show deprecation warning if using the old variable
62
+ if (process.env.GB_USER && !process.env.GB_EMAIL) {
63
+ console.error("⚠️ GB_USER is deprecated. Use GB_EMAIL instead.");
64
+ }
65
+ const emailSchema = z.string().email();
66
+ if (!emailSchema.safeParse(user).success) {
67
+ throw new Error("GB_EMAIL is not a valid email");
68
+ }
69
+ try {
70
+ const users = await fetch(`${baseApiUrl}/api/v1/members?userEmail=${user}`, {
71
+ headers: {
72
+ Authorization: `Bearer ${apiKey}`,
73
+ },
74
+ });
75
+ await handleResNotOk(users);
76
+ const usersData = await users.json();
77
+ if (usersData.members.length === 0) {
78
+ throw new Error(`Email not found in GrowthBook. Update GB_EMAIL environment variable to your email address in GrowthBook.`);
79
+ }
80
+ const userFromGrowthBook = {
81
+ email: usersData.members[0].email,
82
+ name: usersData.members[0].name,
83
+ };
84
+ return userFromGrowthBook;
85
+ }
86
+ catch (error) {
87
+ throw new Error(`Error fetching user from GrowthBook. Please check your GB_EMAIL and GB_API_KEY environment variables.`);
59
88
  }
60
- return user;
61
89
  }
62
90
  export function getDocsMetadata(extension) {
63
91
  switch (extension) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@growthbook/mcp",
3
- "version": "0.1.2",
3
+ "version": "1.0.0",
4
4
  "description": "",
5
5
  "access": "public",
6
6
  "homepage": "https://github.com/growthbook/growthbook-mcp",
@@ -22,13 +22,13 @@
22
22
  "license": "MIT",
23
23
  "packageManager": "pnpm@10.6.1",
24
24
  "dependencies": {
25
- "@modelcontextprotocol/sdk": "^1.13.1",
25
+ "@modelcontextprotocol/sdk": "^1.17.2",
26
26
  "env-paths": "^3.0.0",
27
27
  "zod": "^3.25.67"
28
28
  },
29
29
  "devDependencies": {
30
- "@types/node": "^24.0.4",
31
- "typescript": "^5.8.3"
30
+ "@types/node": "^24.2.1",
31
+ "typescript": "^5.9.2"
32
32
  },
33
33
  "type": "module"
34
34
  }