@growthbook/mcp 1.0.2 → 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.
package/README.md CHANGED
@@ -18,126 +18,5 @@ Use the following env variables to configure the MCP server.
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
 
21
- Find instructions below to add the MCP server to a client. Any client that supports MCP is also compatible. Consult its documentation for how to add the server.
22
21
 
23
- ### Cursor
24
-
25
- 1. Open **Cursor Settings** → **MCP**
26
- 2. Click **Add new global MCP server**
27
- 3. Add an entry for the GrowthBook MCP, following the pattern below:
28
-
29
- ```json
30
- {
31
- "mcpServers": {
32
- "growthbook": {
33
- "command": "npx",
34
- "args": ["-y", "@growthbook/mcp"],
35
- "env": {
36
- "GB_API_KEY": "YOUR_API_KEY",
37
- "GB_API_URL": "YOUR_API_URL",
38
- "GB_APP_ORIGIN": "YOUR_APP_ORIGIN",
39
- "GB_EMAIL": "YOUR_EMAIL"
40
- }
41
- }
42
- }
43
- }
44
- ```
45
-
46
- 3. Save the settings.
47
-
48
- You should now see a green active status after the server successfully connects!
49
-
50
- ### VS Code
51
-
52
- 1. Open **User Settings (JSON)**
53
- 2. Add an MCP entry:
54
-
55
- ```json
56
- "mcp": {
57
- "servers": {
58
- "growthbook": {
59
- "command": "npx",
60
- "args": [
61
- "-y", "@growthbook/mcp"
62
- ],
63
- "env": {
64
- "GB_API_KEY": "YOUR_API_KEY",
65
- "GB_API_URL": "YOUR_API_URL",
66
- "GB_APP_ORIGIN": "YOUR_APP_ORIGIN",
67
- "GB_EMAIL": "YOUR_EMAIL"
68
- }
69
- }
70
- }
71
- }
72
- ```
73
-
74
- 3. Save your settings.
75
-
76
- GrowthBook MCP is now ready to use in VS Code.
77
-
78
- ### Claude Desktop
79
-
80
- 1. **Open Settings** → **Developer**
81
- 2. Click **Edit Config**
82
- 3. Open `claude_desktop_config.json`
83
- 4. Add the following configuration:
84
-
85
- ```json
86
- {
87
- "mcpServers": {
88
- "growthbook": {
89
- "command": "npx",
90
- "args": ["-y", "@growthbook/mcp"],
91
- "env": {
92
- "GB_API_KEY": "YOUR_API_KEY",
93
- "GB_API_URL": "YOUR_API_URL",
94
- "GB_APP_ORIGIN": "YOUR_APP_ORIGIN",
95
- "GB_EMAIL": "YOUR_EMAIL"
96
- }
97
- }
98
- }
99
- }
100
- ```
101
-
102
- 5. Save the config and restart Claude
103
-
104
- A hammer icon should appear in the chat window, indicating that your GrowthBook MCP server is connected and available!
105
-
106
- ---
107
-
108
- ## Tools
109
-
110
- - **Feature Flags**
111
-
112
- - `create_feature_flag`: Create, add, or wrap an element with a feature flag. Specify key, type, default value, and metadata.
113
- - `get_feature_flags`: List all feature flags in your GrowthBook instance.
114
- - `get_single_feature_flag`: Fetch details for a specific feature flag by ID.
115
- - `get_stale_safe_rollouts`: List all safe rollout rules that have been rolled back or released.
116
- - `create_force_rule`: Create a feature flag with a targeting condition.
117
- - `generate_flag_types`: Generates types for feature flags
118
-
119
- - **Experiments**
120
-
121
- - `get_experiments`: List all experiments in GrowthBook.
122
- - `get_experiment`: Fetch details for a specific experiment by ID.
123
- - `get_attributes`: List all user attributes tracked in GrowthBook (useful for targeting).
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.
128
-
129
- - **Environments**
130
-
131
- - `get_environments`: List all environments (e.g., production, staging) configured in GrowthBook.
132
-
133
- - **Projects**
134
-
135
- - `get_projects`: List all projects in your GrowthBook instance.
136
-
137
- - **SDK Connections**
138
-
139
- - `get_sdk_connections`: List all SDK connections (how GrowthBook connects to your apps).
140
- - `create_sdk_connection`: Create a new SDK connection for your app, specifying language and environment.
141
-
142
- - **Documentation Search**
143
- - `search_growthbook_docs`: Search the GrowthBook documentation for information on how to use a feature, by keyword or question.
22
+ Add the MCP server to your AI tool of choice. See the [official docs](https://docs.growthbook.io/integrations/mcp) for complete a complete guide.
package/build/index.js CHANGED
@@ -9,6 +9,7 @@ import { registerSdkConnectionTools } from "./tools/sdk-connections.js";
9
9
  import { getApiKey, getApiUrl, getAppOrigin } from "./utils.js";
10
10
  import { registerSearchTools } from "./tools/search.js";
11
11
  import { registerDefaultsTools } from "./tools/defaults.js";
12
+ import { registerMetricsTools } from "./tools/metrics.js";
12
13
  export const baseApiUrl = getApiUrl();
13
14
  export const apiKey = getApiKey();
14
15
  export const appOrigin = getAppOrigin();
@@ -87,6 +88,13 @@ registerDefaultsTools({
87
88
  baseApiUrl,
88
89
  apiKey,
89
90
  });
91
+ registerMetricsTools({
92
+ server,
93
+ baseApiUrl,
94
+ apiKey,
95
+ appOrigin,
96
+ user,
97
+ });
90
98
  // Start receiving messages on stdin and sending messages on stdout
91
99
  const transport = new StdioServerTransport();
92
100
  await server.connect(transport);
@@ -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
  }
@@ -1,108 +1,75 @@
1
1
  import { z } from "zod";
2
- import { generateLinkToGrowthBook, getDocsMetadata, handleResNotOk, SUPPORTED_FILE_EXTENSIONS, } from "../utils.js";
2
+ import { generateLinkToGrowthBook, getDocsMetadata, handleResNotOk, SUPPORTED_FILE_EXTENSIONS, paginationSchema, } from "../utils.js";
3
3
  import { getDefaults } from "./defaults.js";
4
4
  export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin, user, }) {
5
5
  /**
6
6
  * Tool: get_experiments
7
7
  */
8
- server.tool("get_experiments", "Fetches all experiments from the GrowthBook API", {
9
- limit: z.number().optional().default(100),
10
- offset: z.number().optional().default(0),
11
- }, async ({ limit, offset }) => {
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(),
13
+ ...paginationSchema,
14
+ }, async ({ limit, offset, mostRecent, project }) => {
12
15
  try {
13
- const queryParams = new URLSearchParams({
14
- limit: limit?.toString(),
15
- offset: offset?.toString(),
16
- });
17
- const res = await fetch(`${baseApiUrl}/api/v1/experiments?${queryParams.toString()}`, {
16
+ // Default behavior
17
+ if (!mostRecent || offset > 0) {
18
+ const defaultQueryParams = new URLSearchParams({
19
+ limit: limit.toString(),
20
+ offset: offset.toString(),
21
+ });
22
+ if (project) {
23
+ defaultQueryParams.append("projectId", project);
24
+ }
25
+ const defaultRes = await fetch(`${baseApiUrl}/api/v1/experiments?${defaultQueryParams.toString()}`, {
26
+ headers: {
27
+ Authorization: `Bearer ${apiKey}`,
28
+ "Content-Type": "application/json",
29
+ },
30
+ });
31
+ await handleResNotOk(defaultRes);
32
+ const data = await defaultRes.json();
33
+ return {
34
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
35
+ };
36
+ }
37
+ // Most recent behavior
38
+ const countRes = await fetch(`${baseApiUrl}/api/v1/experiments?limit=1`, {
18
39
  headers: {
19
40
  Authorization: `Bearer ${apiKey}`,
20
- "Content-Type": "application/json",
21
41
  },
22
42
  });
23
- await handleResNotOk(res);
24
- const data = await res.json();
25
- return {
26
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
27
- };
28
- }
29
- catch (error) {
30
- throw new Error(`Error fetching experiments: ${error}`);
31
- }
32
- });
33
- /**
34
- * Tool: create_force_rule
35
- */
36
- 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.", {
37
- featureId: z
38
- .string()
39
- .describe("The ID of the feature to create the rule on"),
40
- description: z.string().optional(),
41
- condition: z
42
- .string()
43
- .describe("Applied to everyone by default. Write conditions in MongoDB-style query syntax.")
44
- .optional(),
45
- value: z
46
- .string()
47
- .describe("The type of the value should match the feature type"),
48
- fileExtension: z
49
- .enum(SUPPORTED_FILE_EXTENSIONS)
50
- .describe("The extension of the current file. If it's unclear, ask the user."),
51
- }, async ({ featureId, description, condition, value, fileExtension }) => {
52
- try {
53
- // Fetch feature defaults first and surface to user
54
- const defaults = await getDefaults(apiKey, baseApiUrl);
55
- const defaultEnvironments = defaults.environments;
56
- const payload = {
57
- // Loop through the environments and create a rule for each one keyed by environment name
58
- environments: defaultEnvironments.reduce((acc, env) => {
59
- acc[env] = {
60
- enabled: false,
61
- rules: [
62
- {
63
- type: "force",
64
- description,
65
- condition,
66
- value,
67
- },
68
- ],
69
- };
70
- return acc;
71
- }, {}),
72
- };
73
- const res = await fetch(`${baseApiUrl}/api/v1/features/${featureId}`, {
74
- method: "POST",
43
+ await handleResNotOk(countRes);
44
+ const countData = await countRes.json();
45
+ const total = countData.total;
46
+ const calculatedOffset = Math.max(0, total - limit);
47
+ const mostRecentQueryParams = new URLSearchParams({
48
+ limit: limit.toString(),
49
+ offset: calculatedOffset.toString(),
50
+ });
51
+ if (project) {
52
+ mostRecentQueryParams.append("projectId", project);
53
+ }
54
+ const mostRecentRes = await fetch(`${baseApiUrl}/api/v1/experiments?${mostRecentQueryParams.toString()}`, {
75
55
  headers: {
76
56
  Authorization: `Bearer ${apiKey}`,
77
- "Content-Type": "application/json",
78
57
  },
79
- body: JSON.stringify(payload),
80
58
  });
81
- await handleResNotOk(res);
82
- const data = await res.json();
83
- const linkToGrowthBook = generateLinkToGrowthBook(appOrigin, "features", featureId);
84
- const { docs, language, stub } = getDocsMetadata(fileExtension);
85
- const text = `This is the API response: ${JSON.stringify(data, null, 2)}
86
-
87
- Additionally, here is a template of what to show to the user:
88
-
89
- **✅ Your feature flag \`my-flag-name\` is ready!.**
90
- [View it in GrowthBook](${linkToGrowthBook})
91
-
92
- **How to use it in your code:**
93
- (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)
94
-
95
- ${stub}
96
-
97
- **Want to learn more?**
98
- See the [GrowthBook ${language} docs](${docs})
99
- `;
59
+ await handleResNotOk(mostRecentRes);
60
+ const mostRecentData = await mostRecentRes.json();
61
+ if (mostRecentData.experiments &&
62
+ Array.isArray(mostRecentData.experiments)) {
63
+ mostRecentData.experiments = mostRecentData.experiments.reverse();
64
+ }
100
65
  return {
101
- content: [{ type: "text", text }],
66
+ content: [
67
+ { type: "text", text: JSON.stringify(mostRecentData, null, 2) },
68
+ ],
102
69
  };
103
70
  }
104
71
  catch (error) {
105
- throw new Error(`Error creating force rule: ${error}`);
72
+ throw new Error(`Error fetching experiments: ${error}`);
106
73
  }
107
74
  });
108
75
  /**
@@ -169,7 +136,6 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
169
136
  .string()
170
137
  .optional()
171
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."),
172
- value: z.string().describe("The default value of the experiment."),
173
139
  variations: z
174
140
  .array(z.object({
175
141
  name: z
@@ -185,13 +151,17 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
185
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."),
186
152
  }))
187
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(),
188
158
  fileExtension: z
189
159
  .enum(SUPPORTED_FILE_EXTENSIONS)
190
160
  .describe("The extension of the current file. If it's unclear, ask the user."),
191
161
  confirmedDefaultsReviewed: z
192
162
  .boolean()
193
163
  .describe("Set to true to confirm you have called get_defaults and reviewed the output to guide these parameters."),
194
- }, async ({ description, hypothesis, name, variations, fileExtension, confirmedDefaultsReviewed, }) => {
164
+ }, async ({ description, hypothesis, name, variations, fileExtension, confirmedDefaultsReviewed, project, }) => {
195
165
  if (!confirmedDefaultsReviewed) {
196
166
  return {
197
167
  content: [
@@ -217,6 +187,7 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
217
187
  key: idx.toString(),
218
188
  name: variation.name,
219
189
  })),
190
+ ...(project && { project }),
220
191
  };
221
192
  try {
222
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, } 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,21 +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.", {
101
- limit: z.number().optional().default(100),
102
- offset: z.number().optional().default(0),
103
- }, async ({ limit, offset }) => {
161
+ project: featureFlagSchema.project.optional(),
162
+ ...paginationSchema,
163
+ }, async ({ limit, offset, project }) => {
104
164
  try {
105
165
  const queryParams = new URLSearchParams({
106
166
  limit: limit?.toString(),
107
167
  offset: offset?.toString(),
108
168
  });
169
+ if (project) {
170
+ queryParams.append("projectId", project);
171
+ }
109
172
  const res = await fetch(`${baseApiUrl}/api/v1/features?${queryParams.toString()}`, {
110
173
  headers: {
111
174
  Authorization: `Bearer ${apiKey}`,
@@ -126,14 +189,10 @@ export function registerFeatureTools({ server, baseApiUrl, apiKey, appOrigin, us
126
189
  * Tool: get_single_feature_flag
127
190
  */
128
191
  server.tool("get_single_feature_flag", "Fetches a specific feature flag from the GrowthBook API", {
129
- id: z.string().describe("The ID of the feature flag"),
130
- project: z.string().optional(),
131
- }, async ({ id, project }) => {
192
+ id: featureFlagSchema.id,
193
+ }, async ({ id }) => {
132
194
  try {
133
- const queryParams = new URLSearchParams();
134
- if (project)
135
- queryParams.append("project", project);
136
- const res = await fetch(`${baseApiUrl}/api/v1/features/${id}?${queryParams.toString()}`, {
195
+ const res = await fetch(`${baseApiUrl}/api/v1/features/${id}`, {
137
196
  headers: {
138
197
  Authorization: `Bearer ${apiKey}`,
139
198
  "Content-Type": "application/json",
@@ -144,11 +203,11 @@ export function registerFeatureTools({ server, baseApiUrl, apiKey, appOrigin, us
144
203
  const linkToGrowthBook = generateLinkToGrowthBook(appOrigin, "features", id);
145
204
  const text = `
146
205
  ${JSON.stringify(data.feature, null, 2)}
147
-
206
+
148
207
  Share information about the feature flag with the user. In particular, give details about the enabled environments,
149
- rules for each environment, and the default value. If the feature flag is archived or doesnt exist, inform the user and
150
- ask if they want to remove references to the feature flag from the codebase.
151
-
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
+
152
211
  [View it in GrowthBook](${linkToGrowthBook})
153
212
  `;
154
213
  return {
@@ -194,14 +253,14 @@ export function registerFeatureTools({ server, baseApiUrl, apiKey, appOrigin, us
194
253
  });
195
254
  });
196
255
  const text = `
197
- ${JSON.stringify(filteredSafeRollouts, null, 2)}
198
-
199
- Share information about the rolled-back or released safe rollout rules with the user. Safe Rollout rules are stored under
200
- environmentSettings, keyed by environment and are within the rules array with a type of "safe-rollout". Ask the user if they
201
- would like to remove references to the feature associated with the rolled-back or released safe rollout rules and if they do,
202
- remove the references and associated GrowthBook code and replace the values with controlValue if the safe rollout rule is rolled-back or with the
203
- variationValue if the safe rollout is released. In addition to the current file, you may need to update other files in the codebase.
204
- `;
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
+ `;
205
264
  return {
206
265
  content: [{ type: "text", text }],
207
266
  };
@@ -0,0 +1,94 @@
1
+ import { z } from "zod";
2
+ import { generateLinkToGrowthBook, handleResNotOk, paginationSchema, } from "../utils.js";
3
+ export function registerMetricsTools({ server, baseApiUrl, apiKey, appOrigin, }) {
4
+ /**
5
+ * Tool: get_metrics
6
+ */
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(),
12
+ ...paginationSchema,
13
+ }, async ({ limit, offset, project }) => {
14
+ try {
15
+ const queryParams = new URLSearchParams({
16
+ limit: limit?.toString(),
17
+ offset: offset?.toString(),
18
+ });
19
+ if (project) {
20
+ queryParams.append("projectId", project);
21
+ }
22
+ const metricsRes = await fetch(`${baseApiUrl}/api/v1/metrics?${queryParams.toString()}`, {
23
+ headers: {
24
+ Authorization: `Bearer ${apiKey}`,
25
+ "Content-Type": "application/json",
26
+ },
27
+ });
28
+ await handleResNotOk(metricsRes);
29
+ const metricsData = await metricsRes.json();
30
+ const factMetricRes = await fetch(`${baseApiUrl}/api/v1/fact-metrics?${queryParams.toString()}`, {
31
+ headers: {
32
+ Authorization: `Bearer ${apiKey}`,
33
+ "Content-Type": "application/json",
34
+ },
35
+ });
36
+ await handleResNotOk(factMetricRes);
37
+ const factMetricData = await factMetricRes.json();
38
+ const metricData = {
39
+ metrics: metricsData,
40
+ factMetrics: factMetricData,
41
+ };
42
+ return {
43
+ content: [
44
+ { type: "text", text: JSON.stringify(metricData, null, 2) },
45
+ ],
46
+ };
47
+ }
48
+ catch (error) {
49
+ throw new Error(`Error fetching metrics: ${error}`);
50
+ }
51
+ });
52
+ /**
53
+ * Tool: get_metric
54
+ */
55
+ server.tool("get_metric", "Fetches a metric from the GrowthBook API", {
56
+ metricId: z.string().describe("The ID of the metric to get"),
57
+ }, async ({ metricId }) => {
58
+ try {
59
+ let res;
60
+ if (metricId.startsWith("fact__")) {
61
+ res = await fetch(`${baseApiUrl}/api/v1/fact-metrics/${metricId}`, {
62
+ headers: {
63
+ Authorization: `Bearer ${apiKey}`,
64
+ "Content-Type": "application/json",
65
+ },
66
+ });
67
+ }
68
+ else {
69
+ res = await fetch(`${baseApiUrl}/api/v1/metrics/${metricId}`, {
70
+ headers: {
71
+ Authorization: `Bearer ${apiKey}`,
72
+ "Content-Type": "application/json",
73
+ },
74
+ });
75
+ }
76
+ await handleResNotOk(res);
77
+ const data = await res.json();
78
+ const linkToGrowthBook = generateLinkToGrowthBook(appOrigin, data.factMetric ? "fact-metrics" : "metric", metricId);
79
+ return {
80
+ content: [
81
+ {
82
+ type: "text",
83
+ text: JSON.stringify(data, null, 2) +
84
+ `\n**Critical** Show the user the link to the metric in GrowthBook: [View the metric in GrowthBook](${linkToGrowthBook})
85
+ `,
86
+ },
87
+ ],
88
+ };
89
+ }
90
+ catch (error) {
91
+ throw new Error(`Error fetching metric: ${error}`);
92
+ }
93
+ });
94
+ }
@@ -1,12 +1,10 @@
1
- import { z } from "zod";
2
- import { handleResNotOk } from "../utils.js";
1
+ import { handleResNotOk, paginationSchema, } from "../utils.js";
3
2
  /**
4
3
  * Tool: get_projects
5
4
  */
6
5
  export function registerProjectTools({ server, baseApiUrl, apiKey, }) {
7
6
  server.tool("get_projects", "Fetches all projects from the GrowthBook API", {
8
- limit: z.number().optional().default(100),
9
- offset: z.number().optional().default(0),
7
+ ...paginationSchema,
10
8
  }, async ({ limit, offset }) => {
11
9
  const queryParams = new URLSearchParams({
12
10
  limit: limit.toString(),
@@ -26,9 +24,7 @@ export function registerProjectTools({ server, baseApiUrl, apiKey, }) {
26
24
  };
27
25
  }
28
26
  catch (error) {
29
- return {
30
- content: [{ type: "text", text: `Error: ${error}` }],
31
- };
27
+ throw new Error(`Error fetching projects: ${error}`);
32
28
  }
33
29
  });
34
30
  }
@@ -1,18 +1,24 @@
1
1
  import { z } from "zod";
2
- import { handleResNotOk } from "../utils.js";
2
+ import { handleResNotOk, paginationSchema, } from "../utils.js";
3
3
  export function registerSdkConnectionTools({ server, baseApiUrl, apiKey, }) {
4
4
  /**
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
- limit: z.number().optional().default(100),
9
- offset: z.number().optional().default(0),
10
- }, async ({ limit, offset }) => {
8
+ project: z
9
+ .string()
10
+ .describe("The ID of the project to filter SDK connections by")
11
+ .optional(),
12
+ ...paginationSchema,
13
+ }, async ({ limit, offset, project }) => {
11
14
  try {
12
15
  const queryParams = new URLSearchParams({
13
16
  limit: limit?.toString(),
14
17
  offset: offset?.toString(),
15
18
  });
19
+ if (project) {
20
+ queryParams.append("projectId", project);
21
+ }
16
22
  const res = await fetch(`${baseApiUrl}/api/v1/sdk-connections?${queryParams.toString()}`, {
17
23
  headers: {
18
24
  Authorization: `Bearer ${apiKey}`,
@@ -61,12 +67,16 @@ export function registerSdkConnectionTools({ server, baseApiUrl, apiKey, }) {
61
67
  "edge-other",
62
68
  "other",
63
69
  ])
64
- .describe("The language of the SDK. Either 'javascript' or 'typescript'."),
70
+ .describe("The language or platform for the SDK connection."),
65
71
  environment: z
66
72
  .string()
67
73
  .optional()
68
74
  .describe("The environment associated with the SDK connection."),
69
- }, 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 }) => {
70
80
  if (!environment) {
71
81
  try {
72
82
  const res = await fetch(`${baseApiUrl}/api/v1/environments`, {
@@ -95,6 +105,7 @@ export function registerSdkConnectionTools({ server, baseApiUrl, apiKey, }) {
95
105
  name,
96
106
  language,
97
107
  environment,
108
+ ...(projects && { projects }),
98
109
  };
99
110
  try {
100
111
  const res = await fetch(`${baseApiUrl}/api/v1/sdk-connections`, {
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 = [
@@ -172,3 +173,40 @@ export async function searchGrowthBookDocs(query) {
172
173
  export function generateLinkToGrowthBook(appOrigin, resource, id) {
173
174
  return `${appOrigin}/${resource}/${id}`;
174
175
  }
176
+ // Reusable pagination schema for GrowthBook API tools
177
+ export const paginationSchema = {
178
+ limit: z
179
+ .number()
180
+ .min(1)
181
+ .max(100)
182
+ .default(100)
183
+ .describe("The number of items to fetch (1-100)"),
184
+ offset: z
185
+ .number()
186
+ .min(0)
187
+ .default(0)
188
+ .describe("The number of items to skip. For example, set to 100 to fetch the second page with default limit. Note: The API returns items in chronological order (oldest first) by default."),
189
+ mostRecent: z
190
+ .boolean()
191
+ .default(false)
192
+ .describe("When true, fetches the most recent items and returns them newest-first. When false (default), returns oldest items first."),
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.0.2",
4
- "description": "",
3
+ "version": "1.2.0",
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"
@@ -17,7 +18,13 @@
17
18
  "files": [
18
19
  "build"
19
20
  ],
20
- "keywords": [],
21
+ "keywords": [
22
+ "growthbook",
23
+ "mcp",
24
+ "modelcontextprotocol",
25
+ "featureflags",
26
+ "experiments"
27
+ ],
21
28
  "author": "GrowthBook",
22
29
  "license": "MIT",
23
30
  "packageManager": "pnpm@10.6.1",