@growthbook/mcp 1.1.0 → 1.2.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.
@@ -18,9 +18,7 @@ export function registerEnvironmentTools({ server, baseApiUrl, apiKey, }) {
18
18
  };
19
19
  }
20
20
  catch (error) {
21
- return {
22
- content: [{ type: "text", text: `Error: ${error}` }],
23
- };
21
+ throw new Error(`Error fetching environments: ${error}`);
24
22
  }
25
23
  });
26
24
  }
@@ -6,8 +6,12 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
6
6
  * Tool: get_experiments
7
7
  */
8
8
  server.tool("get_experiments", "Fetches experiments from the GrowthBook API", {
9
+ project: z
10
+ .string()
11
+ .describe("The ID of the project to filter experiments by")
12
+ .optional(),
9
13
  ...paginationSchema,
10
- }, async ({ limit, offset, mostRecent }) => {
14
+ }, async ({ limit, offset, mostRecent, project }) => {
11
15
  try {
12
16
  // Default behavior
13
17
  if (!mostRecent || offset > 0) {
@@ -15,6 +19,9 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
15
19
  limit: limit.toString(),
16
20
  offset: offset.toString(),
17
21
  });
22
+ if (project) {
23
+ defaultQueryParams.append("projectId", project);
24
+ }
18
25
  const defaultRes = await fetch(`${baseApiUrl}/api/v1/experiments?${defaultQueryParams.toString()}`, {
19
26
  headers: {
20
27
  Authorization: `Bearer ${apiKey}`,
@@ -41,6 +48,9 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
41
48
  limit: limit.toString(),
42
49
  offset: calculatedOffset.toString(),
43
50
  });
51
+ if (project) {
52
+ mostRecentQueryParams.append("projectId", project);
53
+ }
44
54
  const mostRecentRes = await fetch(`${baseApiUrl}/api/v1/experiments?${mostRecentQueryParams.toString()}`, {
45
55
  headers: {
46
56
  Authorization: `Bearer ${apiKey}`,
@@ -62,81 +72,6 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
62
72
  throw new Error(`Error fetching experiments: ${error}`);
63
73
  }
64
74
  });
65
- /**
66
- * Tool: create_force_rule
67
- */
68
- server.tool("create_force_rule", "Create a new force rule on an existing feature. If the existing feature isn't apparent, create a new feature using create_feature_flag first. A force rule sets a feature to a specific value based on a condition. For A/B tests and experiments, use create_experiment instead.", {
69
- featureId: z
70
- .string()
71
- .describe("The ID of the feature to create the rule on"),
72
- description: z.string().optional(),
73
- condition: z
74
- .string()
75
- .describe("Applied to everyone by default. Write conditions in MongoDB-style query syntax.")
76
- .optional(),
77
- value: z
78
- .string()
79
- .describe("The type of the value should match the feature type"),
80
- fileExtension: z
81
- .enum(SUPPORTED_FILE_EXTENSIONS)
82
- .describe("The extension of the current file. If it's unclear, ask the user."),
83
- }, async ({ featureId, description, condition, value, fileExtension }) => {
84
- try {
85
- // Fetch feature defaults first and surface to user
86
- const defaults = await getDefaults(apiKey, baseApiUrl);
87
- const defaultEnvironments = defaults.environments;
88
- const payload = {
89
- // Loop through the environments and create a rule for each one keyed by environment name
90
- environments: defaultEnvironments.reduce((acc, env) => {
91
- acc[env] = {
92
- enabled: false,
93
- rules: [
94
- {
95
- type: "force",
96
- description,
97
- condition,
98
- value,
99
- },
100
- ],
101
- };
102
- return acc;
103
- }, {}),
104
- };
105
- const res = await fetch(`${baseApiUrl}/api/v1/features/${featureId}`, {
106
- method: "POST",
107
- headers: {
108
- Authorization: `Bearer ${apiKey}`,
109
- "Content-Type": "application/json",
110
- },
111
- body: JSON.stringify(payload),
112
- });
113
- await handleResNotOk(res);
114
- const data = await res.json();
115
- const linkToGrowthBook = generateLinkToGrowthBook(appOrigin, "features", featureId);
116
- const { docs, language, stub } = getDocsMetadata(fileExtension);
117
- const text = `This is the API response: ${JSON.stringify(data, null, 2)}
118
-
119
- Additionally, here is a template of what to show to the user:
120
-
121
- **✅ Your feature flag \`my-flag-name\` is ready!.**
122
- [View it in GrowthBook](${linkToGrowthBook})
123
-
124
- **How to use it in your code:**
125
- (To the LLM agent. Use the text below to properly add the feature flag to the codebase. Also, surface the relevant docs/implemenation to the user)
126
-
127
- ${stub}
128
-
129
- **Want to learn more?**
130
- See the [GrowthBook ${language} docs](${docs})
131
- `;
132
- return {
133
- content: [{ type: "text", text }],
134
- };
135
- }
136
- catch (error) {
137
- throw new Error(`Error creating force rule: ${error}`);
138
- }
139
- });
140
75
  /**
141
76
  * Tool: get_experiment
142
77
  */
@@ -201,7 +136,6 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
201
136
  .string()
202
137
  .optional()
203
138
  .describe("Experiment hypothesis. Base hypothesis off the examples from get_defaults. If none are available, use a falsifiable statement about what will happen if the experiment succeeds or fails."),
204
- value: z.string().describe("The default value of the experiment."),
205
139
  variations: z
206
140
  .array(z.object({
207
141
  name: z
@@ -217,13 +151,17 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
217
151
  .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."),
218
152
  }))
219
153
  .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."),
154
+ project: z
155
+ .string()
156
+ .describe("The ID of the project to create the experiment in")
157
+ .optional(),
220
158
  fileExtension: z
221
159
  .enum(SUPPORTED_FILE_EXTENSIONS)
222
160
  .describe("The extension of the current file. If it's unclear, ask the user."),
223
161
  confirmedDefaultsReviewed: z
224
162
  .boolean()
225
163
  .describe("Set to true to confirm you have called get_defaults and reviewed the output to guide these parameters."),
226
- }, async ({ description, hypothesis, name, variations, fileExtension, confirmedDefaultsReviewed, }) => {
164
+ }, async ({ description, hypothesis, name, variations, fileExtension, confirmedDefaultsReviewed, project, }) => {
227
165
  if (!confirmedDefaultsReviewed) {
228
166
  return {
229
167
  content: [
@@ -249,6 +187,7 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
249
187
  key: idx.toString(),
250
188
  name: variation.name,
251
189
  })),
190
+ ...(project && { project }),
252
191
  };
253
192
  try {
254
193
  const experimentRes = await fetch(`${baseApiUrl}/api/v1/experiments`, {
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { getDocsMetadata, handleResNotOk, generateLinkToGrowthBook, SUPPORTED_FILE_EXTENSIONS, paginationSchema, } from "../utils.js";
2
+ import { getDocsMetadata, handleResNotOk, generateLinkToGrowthBook, paginationSchema, featureFlagSchema, } from "../utils.js";
3
3
  import { exec } from "child_process";
4
4
  import { getDefaults } from "./defaults.js";
5
5
  export function registerFeatureTools({ server, baseApiUrl, apiKey, appOrigin, user, }) {
@@ -7,25 +7,13 @@ export function registerFeatureTools({ server, baseApiUrl, apiKey, appOrigin, us
7
7
  * Tool: create_feature_flag
8
8
  */
9
9
  server.tool("create_feature_flag", "Creates a new feature flag in GrowthBook and modifies the codebase when relevant.", {
10
- id: z
11
- .string()
12
- .regex(/^[a-zA-Z0-9_-]+$/, "Feature key can only include letters, numbers, hyphens, and underscores.")
13
- .describe("A unique key name for the feature"),
14
- description: z
15
- .string()
16
- .optional()
17
- .default("")
18
- .describe("A brief description of the feature flag"),
19
- valueType: z
20
- .enum(["string", "number", "boolean", "json"])
21
- .describe("The value type the feature flag will return"),
22
- defaultValue: z
23
- .string()
24
- .describe("The default value of the feature flag"),
25
- fileExtension: z
26
- .enum(SUPPORTED_FILE_EXTENSIONS)
27
- .describe("The extension of the current file. If it's unclear, ask the user."),
28
- }, async ({ id, description, valueType, defaultValue, fileExtension }) => {
10
+ id: featureFlagSchema.id,
11
+ valueType: featureFlagSchema.valueType,
12
+ defaultValue: featureFlagSchema.defaultValue,
13
+ description: featureFlagSchema.description.optional().default(""),
14
+ project: featureFlagSchema.project.optional(),
15
+ fileExtension: featureFlagSchema.fileExtension,
16
+ }, async ({ id, valueType, defaultValue, description, project, fileExtension, }) => {
29
17
  // get environments
30
18
  let environments = [];
31
19
  const defaults = await getDefaults(apiKey, baseApiUrl);
@@ -57,6 +45,7 @@ export function registerFeatureTools({ server, baseApiUrl, apiKey, appOrigin, us
57
45
  };
58
46
  return acc;
59
47
  }, {}),
48
+ ...(project && { project }),
60
49
  };
61
50
  try {
62
51
  const res = await fetch(`${baseApiUrl}/api/v1/features`, {
@@ -72,6 +61,77 @@ export function registerFeatureTools({ server, baseApiUrl, apiKey, appOrigin, us
72
61
  const { docs, language, stub } = getDocsMetadata(fileExtension);
73
62
  const linkToGrowthBook = generateLinkToGrowthBook(appOrigin, "features", id);
74
63
  const text = `This is the API response: ${JSON.stringify(data, null, 2)}
64
+
65
+ Additionally, here is a template of what to show to the user:
66
+
67
+ **✅ Your feature flag \`my-flag-name\` is ready!**
68
+ [View it in GrowthBook](${linkToGrowthBook})
69
+
70
+ **How to use it in your code:**
71
+ (To the LLM agent. Use the text below to properly add the feature flag to the codebase. Also, surface the relevant docs/implemenation to the user)
72
+
73
+ ${stub}
74
+
75
+ **Want to learn more?**
76
+ See the [GrowthBook ${language} docs](${docs})
77
+ `;
78
+ return {
79
+ content: [{ type: "text", text }],
80
+ };
81
+ }
82
+ catch (error) {
83
+ throw new Error(`Error creating feature flag: ${error}`);
84
+ }
85
+ });
86
+ /**
87
+ * Tool: create_force_rule
88
+ */
89
+ server.tool("create_force_rule", "Create a new force rule on an existing feature. If the existing feature isn't apparent, create a new feature using create_feature_flag first. A force rule sets a feature to a specific value based on a condition. For A/B tests and experiments, use create_experiment instead.", {
90
+ featureId: featureFlagSchema.id,
91
+ description: featureFlagSchema.description.optional().default(""),
92
+ fileExtension: featureFlagSchema.fileExtension,
93
+ condition: z
94
+ .string()
95
+ .describe("Applied to everyone by default. Write conditions in MongoDB-style query syntax.")
96
+ .optional(),
97
+ value: z
98
+ .string()
99
+ .describe("The type of the value should match the feature type"),
100
+ }, async ({ featureId, description, condition, value, fileExtension }) => {
101
+ try {
102
+ // Fetch feature defaults first and surface to user
103
+ const defaults = await getDefaults(apiKey, baseApiUrl);
104
+ const defaultEnvironments = defaults.environments;
105
+ const payload = {
106
+ // Loop through the environments and create a rule for each one keyed by environment name
107
+ environments: defaultEnvironments.reduce((acc, env) => {
108
+ acc[env] = {
109
+ enabled: false,
110
+ rules: [
111
+ {
112
+ type: "force",
113
+ description,
114
+ condition,
115
+ value,
116
+ },
117
+ ],
118
+ };
119
+ return acc;
120
+ }, {}),
121
+ };
122
+ const res = await fetch(`${baseApiUrl}/api/v1/features/${featureId}`, {
123
+ method: "POST",
124
+ headers: {
125
+ Authorization: `Bearer ${apiKey}`,
126
+ "Content-Type": "application/json",
127
+ },
128
+ body: JSON.stringify(payload),
129
+ });
130
+ await handleResNotOk(res);
131
+ const data = await res.json();
132
+ const linkToGrowthBook = generateLinkToGrowthBook(appOrigin, "features", featureId);
133
+ const { docs, language, stub } = getDocsMetadata(fileExtension);
134
+ const text = `This is the API response: ${JSON.stringify(data, null, 2)}
75
135
 
76
136
  Additionally, here is a template of what to show to the user:
77
137
 
@@ -91,20 +151,24 @@ export function registerFeatureTools({ server, baseApiUrl, apiKey, appOrigin, us
91
151
  };
92
152
  }
93
153
  catch (error) {
94
- throw new Error(`Error creating feature flag: ${error}`);
154
+ throw new Error(`Error creating force rule: ${error}`);
95
155
  }
96
156
  });
97
157
  /**
98
158
  * Tool: get_feature_flags
99
159
  */
100
160
  server.tool("get_feature_flags", "Fetches all feature flags from the GrowthBook API, with optional limit, offset, and project filtering.", {
161
+ project: featureFlagSchema.project.optional(),
101
162
  ...paginationSchema,
102
- }, async ({ limit, offset }) => {
163
+ }, async ({ limit, offset, project }) => {
103
164
  try {
104
165
  const queryParams = new URLSearchParams({
105
166
  limit: limit?.toString(),
106
167
  offset: offset?.toString(),
107
168
  });
169
+ if (project) {
170
+ queryParams.append("projectId", project);
171
+ }
108
172
  const res = await fetch(`${baseApiUrl}/api/v1/features?${queryParams.toString()}`, {
109
173
  headers: {
110
174
  Authorization: `Bearer ${apiKey}`,
@@ -125,14 +189,10 @@ export function registerFeatureTools({ server, baseApiUrl, apiKey, appOrigin, us
125
189
  * Tool: get_single_feature_flag
126
190
  */
127
191
  server.tool("get_single_feature_flag", "Fetches a specific feature flag from the GrowthBook API", {
128
- id: z.string().describe("The ID of the feature flag"),
129
- project: z.string().optional(),
130
- }, async ({ id, project }) => {
192
+ id: featureFlagSchema.id,
193
+ }, async ({ id }) => {
131
194
  try {
132
- const queryParams = new URLSearchParams();
133
- if (project)
134
- queryParams.append("project", project);
135
- const res = await fetch(`${baseApiUrl}/api/v1/features/${id}?${queryParams.toString()}`, {
195
+ const res = await fetch(`${baseApiUrl}/api/v1/features/${id}`, {
136
196
  headers: {
137
197
  Authorization: `Bearer ${apiKey}`,
138
198
  "Content-Type": "application/json",
@@ -143,11 +203,11 @@ export function registerFeatureTools({ server, baseApiUrl, apiKey, appOrigin, us
143
203
  const linkToGrowthBook = generateLinkToGrowthBook(appOrigin, "features", id);
144
204
  const text = `
145
205
  ${JSON.stringify(data.feature, null, 2)}
146
-
206
+
147
207
  Share information about the feature flag with the user. In particular, give details about the enabled environments,
148
- rules for each environment, and the default value. If the feature flag is archived or doesnt exist, inform the user and
149
- ask if they want to remove references to the feature flag from the codebase.
150
-
208
+ rules for each environment, and the default value. If the feature flag is archived or doesnt exist, inform the user and
209
+ ask if they want to remove references to the feature flag from the codebase.
210
+
151
211
  [View it in GrowthBook](${linkToGrowthBook})
152
212
  `;
153
213
  return {
@@ -193,14 +253,14 @@ export function registerFeatureTools({ server, baseApiUrl, apiKey, appOrigin, us
193
253
  });
194
254
  });
195
255
  const text = `
196
- ${JSON.stringify(filteredSafeRollouts, null, 2)}
197
-
198
- Share information about the rolled-back or released safe rollout rules with the user. Safe Rollout rules are stored under
199
- environmentSettings, keyed by environment and are within the rules array with a type of "safe-rollout". Ask the user if they
200
- would like to remove references to the feature associated with the rolled-back or released safe rollout rules and if they do,
201
- remove the references and associated GrowthBook code and replace the values with controlValue if the safe rollout rule is rolled-back or with the
202
- variationValue if the safe rollout is released. In addition to the current file, you may need to update other files in the codebase.
203
- `;
256
+ ${JSON.stringify(filteredSafeRollouts, null, 2)}
257
+
258
+ Share information about the rolled-back or released safe rollout rules with the user. Safe Rollout rules are stored under
259
+ environmentSettings, keyed by environment and are within the rules array with a type of "safe-rollout". Ask the user if they
260
+ would like to remove references to the feature associated with the rolled-back or released safe rollout rules and if they do,
261
+ remove the references and associated GrowthBook code and replace the values with controlValue if the safe rollout rule is rolled-back or with the
262
+ variationValue if the safe rollout is released. In addition to the current file, you may need to update other files in the codebase.
263
+ `;
204
264
  return {
205
265
  content: [{ type: "text", text }],
206
266
  };
@@ -4,14 +4,21 @@ export function registerMetricsTools({ server, baseApiUrl, apiKey, appOrigin, })
4
4
  /**
5
5
  * Tool: get_metrics
6
6
  */
7
- server.tool("get_metrics", "Fetches metrics from the GrowthBook API", {
7
+ server.tool("get_metrics", "Fetches metrics from the GrowthBook API, with optional limit, offset, and project filtering.", {
8
+ project: z
9
+ .string()
10
+ .describe("The ID of the project to filter metrics by")
11
+ .optional(),
8
12
  ...paginationSchema,
9
- }, async ({ limit, offset }) => {
13
+ }, async ({ limit, offset, project }) => {
10
14
  try {
11
15
  const queryParams = new URLSearchParams({
12
16
  limit: limit?.toString(),
13
17
  offset: offset?.toString(),
14
18
  });
19
+ if (project) {
20
+ queryParams.append("projectId", project);
21
+ }
15
22
  const metricsRes = await fetch(`${baseApiUrl}/api/v1/metrics?${queryParams.toString()}`, {
16
23
  headers: {
17
24
  Authorization: `Bearer ${apiKey}`,
@@ -24,9 +24,7 @@ export function registerProjectTools({ server, baseApiUrl, apiKey, }) {
24
24
  };
25
25
  }
26
26
  catch (error) {
27
- return {
28
- content: [{ type: "text", text: `Error: ${error}` }],
29
- };
27
+ throw new Error(`Error fetching projects: ${error}`);
30
28
  }
31
29
  });
32
30
  }
@@ -5,13 +5,20 @@ export function registerSdkConnectionTools({ server, baseApiUrl, apiKey, }) {
5
5
  * Tool: get_sdk_connections
6
6
  */
7
7
  server.tool("get_sdk_connections", "Get all SDK connections. SDK connections are how GrowthBook connects to an app. Users need the client key to fetch features and experiments from the API.", {
8
+ project: z
9
+ .string()
10
+ .describe("The ID of the project to filter SDK connections by")
11
+ .optional(),
8
12
  ...paginationSchema,
9
- }, async ({ limit, offset }) => {
13
+ }, async ({ limit, offset, project }) => {
10
14
  try {
11
15
  const queryParams = new URLSearchParams({
12
16
  limit: limit?.toString(),
13
17
  offset: offset?.toString(),
14
18
  });
19
+ if (project) {
20
+ queryParams.append("projectId", project);
21
+ }
15
22
  const res = await fetch(`${baseApiUrl}/api/v1/sdk-connections?${queryParams.toString()}`, {
16
23
  headers: {
17
24
  Authorization: `Bearer ${apiKey}`,
@@ -60,12 +67,16 @@ export function registerSdkConnectionTools({ server, baseApiUrl, apiKey, }) {
60
67
  "edge-other",
61
68
  "other",
62
69
  ])
63
- .describe("The language of the SDK. Either 'javascript' or 'typescript'."),
70
+ .describe("The language or platform for the SDK connection."),
64
71
  environment: z
65
72
  .string()
66
73
  .optional()
67
74
  .describe("The environment associated with the SDK connection."),
68
- }, async ({ name, language, environment }) => {
75
+ projects: z
76
+ .array(z.string())
77
+ .describe("The projects to create the SDK connection in")
78
+ .optional(),
79
+ }, async ({ name, language, environment, projects }) => {
69
80
  if (!environment) {
70
81
  try {
71
82
  const res = await fetch(`${baseApiUrl}/api/v1/environments`, {
@@ -94,6 +105,7 @@ export function registerSdkConnectionTools({ server, baseApiUrl, apiKey, }) {
94
105
  name,
95
106
  language,
96
107
  environment,
108
+ ...(projects && { projects }),
97
109
  };
98
110
  try {
99
111
  const res = await fetch(`${baseApiUrl}/api/v1/sdk-connections`, {
package/build/utils.js CHANGED
@@ -191,3 +191,22 @@ export const paginationSchema = {
191
191
  .default(false)
192
192
  .describe("When true, fetches the most recent items and returns them newest-first. When false (default), returns oldest items first."),
193
193
  };
194
+ export const featureFlagSchema = {
195
+ id: z
196
+ .string()
197
+ .regex(/^[a-zA-Z0-9_.:|_-]+$/, "Feature key can only include letters, numbers, and the characters _, -, ., :, and |")
198
+ .describe("A unique key name for the feature"),
199
+ valueType: z
200
+ .enum(["string", "number", "boolean", "json"])
201
+ .describe("The value type the feature flag will return"),
202
+ defaultValue: z.string().describe("The default value of the feature flag"),
203
+ description: z.string().describe("A brief description of the feature flag"),
204
+ archived: z.boolean().describe("Whether the feature flag should be archived"),
205
+ project: z
206
+ .string()
207
+ .describe("The ID of the project to which the feature flag belongs"),
208
+ // Contextual info
209
+ fileExtension: z
210
+ .enum(SUPPORTED_FILE_EXTENSIONS)
211
+ .describe("The extension of the current file. If it's unclear, ask the user."),
212
+ };
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@growthbook/mcp",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "MCP Server for interacting with GrowthBook",
5
5
  "access": "public",
6
6
  "homepage": "https://github.com/growthbook/growthbook-mcp",
7
7
  "scripts": {
8
8
  "test": "echo \"Error: no test specified\" && exit 1",
9
9
  "build": "tsc",
10
+ "dev": "tsc --watch",
10
11
  "bump:patch": "pnpm version patch --no-git-tag-version",
11
12
  "bump:minor": "pnpm version minor --no-git-tag-version",
12
13
  "bump:major": "pnpm version major --no-git-tag-version"