@growthbook/mcp 1.1.0 → 1.3.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/package.json CHANGED
@@ -1,21 +1,23 @@
1
1
  {
2
2
  "name": "@growthbook/mcp",
3
- "version": "1.1.0",
3
+ "version": "1.3.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
- "bump:patch": "pnpm version patch --no-git-tag-version",
11
- "bump:minor": "pnpm version minor --no-git-tag-version",
12
- "bump:major": "pnpm version major --no-git-tag-version"
10
+ "dev": "tsc --watch",
11
+ "bump:patch": "npm version patch --no-git-tag-version",
12
+ "bump:minor": "npm version minor --no-git-tag-version",
13
+ "bump:major": "npm version major --no-git-tag-version",
14
+ "mcpb:build": "npx -y @anthropic-ai/mcpb -- pack"
13
15
  },
14
16
  "bin": {
15
- "mcp": "build/index.js"
17
+ "mcp": "server/index.js"
16
18
  },
17
19
  "files": [
18
- "build"
20
+ "server"
19
21
  ],
20
22
  "keywords": [
21
23
  "growthbook",
@@ -26,7 +28,6 @@
26
28
  ],
27
29
  "author": "GrowthBook",
28
30
  "license": "MIT",
29
- "packageManager": "pnpm@10.6.1",
30
31
  "dependencies": {
31
32
  "@modelcontextprotocol/sdk": "^1.17.2",
32
33
  "env-paths": "^3.0.0",
@@ -10,6 +10,7 @@ import { getApiKey, getApiUrl, getAppOrigin } from "./utils.js";
10
10
  import { registerSearchTools } from "./tools/search.js";
11
11
  import { registerDefaultsTools } from "./tools/defaults.js";
12
12
  import { registerMetricsTools } from "./tools/metrics.js";
13
+ import { registerExperimentAnalysisPrompt } from "./prompts/experiment-analysis.js";
13
14
  export const baseApiUrl = getApiUrl();
14
15
  export const apiKey = getApiKey();
15
16
  export const appOrigin = getAppOrigin();
@@ -95,6 +96,15 @@ registerMetricsTools({
95
96
  appOrigin,
96
97
  user,
97
98
  });
99
+ registerExperimentAnalysisPrompt({
100
+ server,
101
+ });
98
102
  // Start receiving messages on stdin and sending messages on stdout
99
103
  const transport = new StdioServerTransport();
100
- await server.connect(transport);
104
+ try {
105
+ await server.connect(transport);
106
+ }
107
+ catch (error) {
108
+ console.error(error);
109
+ process.exit(1);
110
+ }
@@ -0,0 +1,13 @@
1
+ export function registerExperimentAnalysisPrompt({ server, }) {
2
+ server.prompt("experiment-analysis", "Analyze recent experiments and give me actionable advice", () => ({
3
+ messages: [
4
+ {
5
+ role: "user",
6
+ content: {
7
+ type: "text",
8
+ text: "Use GrowthBook to fetch my recent experiments. Analyze the experiments and tell me:\n\n1. Which experiment types are actually worth running vs. theater?\n\n2. What's the one pattern in our losses that we're blind to?\n\n3. If you could only run 3 experiments next quarter based on these results, what would they be and why?\n\n4. What's the biggest methodological risk in our current approach that could be invalidating results?\n\nBe specific. Use the actual data. Don't give me generic advice.",
9
+ },
10
+ },
11
+ ],
12
+ }));
13
+ }
@@ -249,7 +249,9 @@ export async function getDefaults(apiKey, baseApiUrl) {
249
249
  * Tool: get_defaults
250
250
  */
251
251
  export async function registerDefaultsTools({ server, baseApiUrl, apiKey, }) {
252
- server.tool("get_defaults", "Get the default values for experiments, including hypothesis, description, datasource, assignment query, and environments.", {}, async () => {
252
+ server.tool("get_defaults", "Get the default values for experiments, including hypothesis, description, datasource, assignment query, and environments.", {}, {
253
+ readOnlyHint: true,
254
+ }, async () => {
253
255
  const defaults = await getDefaults(apiKey, baseApiUrl);
254
256
  return {
255
257
  content: [
@@ -268,6 +270,9 @@ export async function registerDefaultsTools({ server, baseApiUrl, apiKey, }) {
268
270
  environments: z
269
271
  .array(z.string())
270
272
  .describe("List of environment IDs to use as defaults"),
273
+ }, {
274
+ readOnlyHint: false,
275
+ destructiveHint: true,
271
276
  }, async ({ datasourceId, assignmentQueryId, environments }) => {
272
277
  try {
273
278
  const userDefaults = {
@@ -291,7 +296,10 @@ export async function registerDefaultsTools({ server, baseApiUrl, apiKey, }) {
291
296
  throw new Error(`Error setting user defaults: ${error}`);
292
297
  }
293
298
  });
294
- server.tool("clear_user_defaults", "Clear user-defined defaults and revert to automatic defaults.", {}, async () => {
299
+ server.tool("clear_user_defaults", "Clear user-defined defaults and revert to automatic defaults.", {}, {
300
+ readOnlyHint: false,
301
+ destructiveHint: true,
302
+ }, async () => {
295
303
  try {
296
304
  await readFile(userDefaultsFile, "utf8");
297
305
  await unlink(userDefaultsFile);
@@ -3,7 +3,9 @@ import { handleResNotOk } from "../utils.js";
3
3
  * Tool: get_environments
4
4
  */
5
5
  export function registerEnvironmentTools({ server, baseApiUrl, apiKey, }) {
6
- server.tool("get_environments", "Fetches all environments from the GrowthBook API. GrowthBook comes with one environment by default (production), but you can add as many as you need. Feature flags can be enabled and disabled on a per-environment basis. You can also set the default feature state for any new environment. Additionally, you can scope environments to only be available in specific projects, allowing for further control and segmentation over feature delivery.", {}, async () => {
6
+ server.tool("get_environments", "Fetches all environments from the GrowthBook API. GrowthBook comes with one environment by default (production), but you can add as many as you need. Feature flags can be enabled and disabled on a per-environment basis. You can also set the default feature state for any new environment. Additionally, you can scope environments to only be available in specific projects, allowing for further control and segmentation over feature delivery.", {}, {
7
+ readOnlyHint: true,
8
+ }, async () => {
7
9
  try {
8
10
  const res = await fetch(`${baseApiUrl}/api/v1/environments`, {
9
11
  headers: {
@@ -18,9 +20,7 @@ export function registerEnvironmentTools({ server, baseApiUrl, apiKey, }) {
18
20
  };
19
21
  }
20
22
  catch (error) {
21
- return {
22
- content: [{ type: "text", text: `Error: ${error}` }],
23
- };
23
+ throw new Error(`Error fetching environments: ${error}`);
24
24
  }
25
25
  });
26
26
  }
@@ -6,8 +6,18 @@ 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(),
13
+ mode: z
14
+ .enum(["default", "analyze"])
15
+ .default("default")
16
+ .describe("The mode to use to fetch experiments. Default mode returns summary info about experiments. Analyze mode will also fetch experiment results, allowing for better analysis, interpretation, and reporting."),
9
17
  ...paginationSchema,
10
- }, async ({ limit, offset, mostRecent }) => {
18
+ }, {
19
+ readOnlyHint: true,
20
+ }, async ({ limit, offset, mostRecent, project, mode }) => {
11
21
  try {
12
22
  // Default behavior
13
23
  if (!mostRecent || offset > 0) {
@@ -15,6 +25,9 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
15
25
  limit: limit.toString(),
16
26
  offset: offset.toString(),
17
27
  });
28
+ if (project) {
29
+ defaultQueryParams.append("projectId", project);
30
+ }
18
31
  const defaultRes = await fetch(`${baseApiUrl}/api/v1/experiments?${defaultQueryParams.toString()}`, {
19
32
  headers: {
20
33
  Authorization: `Bearer ${apiKey}`,
@@ -23,6 +36,24 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
23
36
  });
24
37
  await handleResNotOk(defaultRes);
25
38
  const data = await defaultRes.json();
39
+ const experiments = data.experiments;
40
+ if (mode === "analyze") {
41
+ for (const [index, experiment] of experiments.entries()) {
42
+ try {
43
+ const resultsRes = await fetch(`${baseApiUrl}/api/v1/experiments/${experiment.id}/results`, {
44
+ headers: {
45
+ Authorization: `Bearer ${apiKey}`,
46
+ },
47
+ });
48
+ await handleResNotOk(resultsRes);
49
+ const resultsData = await resultsRes.json();
50
+ experiments[index].result = resultsData.result;
51
+ }
52
+ catch (error) {
53
+ console.error(`Error fetching results for experiment ${experiment.id} (${experiment.name})`, error);
54
+ }
55
+ }
56
+ }
26
57
  return {
27
58
  content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
28
59
  };
@@ -41,6 +72,9 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
41
72
  limit: limit.toString(),
42
73
  offset: calculatedOffset.toString(),
43
74
  });
75
+ if (project) {
76
+ mostRecentQueryParams.append("projectId", project);
77
+ }
44
78
  const mostRecentRes = await fetch(`${baseApiUrl}/api/v1/experiments?${mostRecentQueryParams.toString()}`, {
45
79
  headers: {
46
80
  Authorization: `Bearer ${apiKey}`,
@@ -51,6 +85,23 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
51
85
  if (mostRecentData.experiments &&
52
86
  Array.isArray(mostRecentData.experiments)) {
53
87
  mostRecentData.experiments = mostRecentData.experiments.reverse();
88
+ if (mode === "analyze") {
89
+ for (const [index, experiment,] of mostRecentData.experiments.entries()) {
90
+ try {
91
+ const resultsRes = await fetch(`${baseApiUrl}/api/v1/experiments/${experiment.id}/results`, {
92
+ headers: {
93
+ Authorization: `Bearer ${apiKey}`,
94
+ },
95
+ });
96
+ await handleResNotOk(resultsRes);
97
+ const resultsData = await resultsRes.json();
98
+ mostRecentData.experiments[index].result = resultsData.result;
99
+ }
100
+ catch (error) {
101
+ console.error(`Error fetching results for experiment ${experiment.id} (${experiment.name})`, error);
102
+ }
103
+ }
104
+ }
54
105
  }
55
106
  return {
56
107
  content: [
@@ -62,87 +113,18 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
62
113
  throw new Error(`Error fetching experiments: ${error}`);
63
114
  }
64
115
  });
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
116
  /**
141
117
  * Tool: get_experiment
142
118
  */
143
119
  server.tool("get_experiment", "Gets a single experiment from GrowthBook", {
144
120
  experimentId: z.string().describe("The ID of the experiment to get"),
145
- }, async ({ experimentId }) => {
121
+ mode: z
122
+ .enum(["default", "analyze"])
123
+ .default("default")
124
+ .describe("The mode to use to fetch the experiment. Default mode returns summary info about the experiment. Analyze mode will also fetch experiment results, allowing for better analysis, interpretation, and reporting."),
125
+ }, {
126
+ readOnlyHint: true,
127
+ }, async ({ experimentId, mode }) => {
146
128
  try {
147
129
  const res = await fetch(`${baseApiUrl}/api/v1/experiments/${experimentId}`, {
148
130
  headers: {
@@ -152,6 +134,22 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
152
134
  });
153
135
  await handleResNotOk(res);
154
136
  const data = await res.json();
137
+ // If analyze mode, fetch results
138
+ if (mode === "analyze") {
139
+ try {
140
+ const resultsRes = await fetch(`${baseApiUrl}/api/v1/experiments/${experimentId}/results`, {
141
+ headers: {
142
+ Authorization: `Bearer ${apiKey}`,
143
+ },
144
+ });
145
+ await handleResNotOk(resultsRes);
146
+ const resultsData = await resultsRes.json();
147
+ data.result = resultsData.result;
148
+ }
149
+ catch (error) {
150
+ console.error(`Error fetching results for experiment ${experimentId}`, error);
151
+ }
152
+ }
155
153
  const linkToGrowthBook = generateLinkToGrowthBook(appOrigin, "experiment", experimentId);
156
154
  const text = `
157
155
  ${JSON.stringify(data, null, 2)}
@@ -169,7 +167,9 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
169
167
  /**
170
168
  * Tool: get_attributes
171
169
  */
172
- server.tool("get_attributes", "Get all attributes", {}, async () => {
170
+ server.tool("get_attributes", "Get all attributes", {}, {
171
+ readOnlyHint: true,
172
+ }, async () => {
173
173
  try {
174
174
  const queryParams = new URLSearchParams();
175
175
  queryParams.append("limit", "100");
@@ -201,7 +201,6 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
201
201
  .string()
202
202
  .optional()
203
203
  .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
204
  variations: z
206
205
  .array(z.object({
207
206
  name: z
@@ -217,13 +216,20 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
217
216
  .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
217
  }))
219
218
  .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."),
219
+ project: z
220
+ .string()
221
+ .describe("The ID of the project to create the experiment in")
222
+ .optional(),
220
223
  fileExtension: z
221
224
  .enum(SUPPORTED_FILE_EXTENSIONS)
222
225
  .describe("The extension of the current file. If it's unclear, ask the user."),
223
226
  confirmedDefaultsReviewed: z
224
227
  .boolean()
225
228
  .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, }) => {
229
+ }, {
230
+ readOnlyHint: false,
231
+ destructiveHint: false,
232
+ }, async ({ description, hypothesis, name, variations, fileExtension, confirmedDefaultsReviewed, project, }) => {
227
233
  if (!confirmedDefaultsReviewed) {
228
234
  return {
229
235
  content: [
@@ -249,6 +255,7 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
249
255
  key: idx.toString(),
250
256
  name: variation.name,
251
257
  })),
258
+ ...(project && { project }),
252
259
  };
253
260
  try {
254
261
  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,16 @@ 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
+ }, {
17
+ readOnlyHint: false,
18
+ destructiveHint: false,
19
+ }, async ({ id, valueType, defaultValue, description, project, fileExtension, }) => {
29
20
  // get environments
30
21
  let environments = [];
31
22
  const defaults = await getDefaults(apiKey, baseApiUrl);
@@ -57,6 +48,7 @@ export function registerFeatureTools({ server, baseApiUrl, apiKey, appOrigin, us
57
48
  };
58
49
  return acc;
59
50
  }, {}),
51
+ ...(project && { project }),
60
52
  };
61
53
  try {
62
54
  const res = await fetch(`${baseApiUrl}/api/v1/features`, {
@@ -72,6 +64,79 @@ export function registerFeatureTools({ server, baseApiUrl, apiKey, appOrigin, us
72
64
  const { docs, language, stub } = getDocsMetadata(fileExtension);
73
65
  const linkToGrowthBook = generateLinkToGrowthBook(appOrigin, "features", id);
74
66
  const text = `This is the API response: ${JSON.stringify(data, null, 2)}
67
+
68
+ Additionally, here is a template of what to show to the user:
69
+
70
+ **✅ Your feature flag \`my-flag-name\` is ready!**
71
+ [View it in GrowthBook](${linkToGrowthBook})
72
+
73
+ **How to use it in your code:**
74
+ (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)
75
+
76
+ ${stub}
77
+
78
+ **Want to learn more?**
79
+ See the [GrowthBook ${language} docs](${docs})
80
+ `;
81
+ return {
82
+ content: [{ type: "text", text }],
83
+ };
84
+ }
85
+ catch (error) {
86
+ throw new Error(`Error creating feature flag: ${error}`);
87
+ }
88
+ });
89
+ /**
90
+ * Tool: create_force_rule
91
+ */
92
+ 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.", {
93
+ featureId: featureFlagSchema.id,
94
+ description: featureFlagSchema.description.optional().default(""),
95
+ fileExtension: featureFlagSchema.fileExtension,
96
+ condition: z
97
+ .string()
98
+ .describe("Applied to everyone by default. Write conditions in MongoDB-style query syntax.")
99
+ .optional(),
100
+ value: z
101
+ .string()
102
+ .describe("The type of the value should match the feature type"),
103
+ }, {
104
+ readOnlyHint: false,
105
+ }, async ({ featureId, description, condition, value, fileExtension }) => {
106
+ try {
107
+ // Fetch feature defaults first and surface to user
108
+ const defaults = await getDefaults(apiKey, baseApiUrl);
109
+ const defaultEnvironments = defaults.environments;
110
+ const payload = {
111
+ // Loop through the environments and create a rule for each one keyed by environment name
112
+ environments: defaultEnvironments.reduce((acc, env) => {
113
+ acc[env] = {
114
+ enabled: false,
115
+ rules: [
116
+ {
117
+ type: "force",
118
+ description,
119
+ condition,
120
+ value,
121
+ },
122
+ ],
123
+ };
124
+ return acc;
125
+ }, {}),
126
+ };
127
+ const res = await fetch(`${baseApiUrl}/api/v1/features/${featureId}`, {
128
+ method: "POST",
129
+ headers: {
130
+ Authorization: `Bearer ${apiKey}`,
131
+ "Content-Type": "application/json",
132
+ },
133
+ body: JSON.stringify(payload),
134
+ });
135
+ await handleResNotOk(res);
136
+ const data = await res.json();
137
+ const linkToGrowthBook = generateLinkToGrowthBook(appOrigin, "features", featureId);
138
+ const { docs, language, stub } = getDocsMetadata(fileExtension);
139
+ const text = `This is the API response: ${JSON.stringify(data, null, 2)}
75
140
 
76
141
  Additionally, here is a template of what to show to the user:
77
142
 
@@ -91,20 +156,26 @@ export function registerFeatureTools({ server, baseApiUrl, apiKey, appOrigin, us
91
156
  };
92
157
  }
93
158
  catch (error) {
94
- throw new Error(`Error creating feature flag: ${error}`);
159
+ throw new Error(`Error creating force rule: ${error}`);
95
160
  }
96
161
  });
97
162
  /**
98
163
  * Tool: get_feature_flags
99
164
  */
100
165
  server.tool("get_feature_flags", "Fetches all feature flags from the GrowthBook API, with optional limit, offset, and project filtering.", {
166
+ project: featureFlagSchema.project.optional(),
101
167
  ...paginationSchema,
102
- }, async ({ limit, offset }) => {
168
+ }, {
169
+ readOnlyHint: true,
170
+ }, async ({ limit, offset, project }) => {
103
171
  try {
104
172
  const queryParams = new URLSearchParams({
105
173
  limit: limit?.toString(),
106
174
  offset: offset?.toString(),
107
175
  });
176
+ if (project) {
177
+ queryParams.append("projectId", project);
178
+ }
108
179
  const res = await fetch(`${baseApiUrl}/api/v1/features?${queryParams.toString()}`, {
109
180
  headers: {
110
181
  Authorization: `Bearer ${apiKey}`,
@@ -125,14 +196,12 @@ export function registerFeatureTools({ server, baseApiUrl, apiKey, appOrigin, us
125
196
  * Tool: get_single_feature_flag
126
197
  */
127
198
  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 }) => {
199
+ id: featureFlagSchema.id,
200
+ }, {
201
+ readOnlyHint: true,
202
+ }, async ({ id }) => {
131
203
  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()}`, {
204
+ const res = await fetch(`${baseApiUrl}/api/v1/features/${id}`, {
136
205
  headers: {
137
206
  Authorization: `Bearer ${apiKey}`,
138
207
  "Content-Type": "application/json",
@@ -143,11 +212,11 @@ export function registerFeatureTools({ server, baseApiUrl, apiKey, appOrigin, us
143
212
  const linkToGrowthBook = generateLinkToGrowthBook(appOrigin, "features", id);
144
213
  const text = `
145
214
  ${JSON.stringify(data.feature, null, 2)}
146
-
215
+
147
216
  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
-
217
+ rules for each environment, and the default value. If the feature flag is archived or doesnt exist, inform the user and
218
+ ask if they want to remove references to the feature flag from the codebase.
219
+
151
220
  [View it in GrowthBook](${linkToGrowthBook})
152
221
  `;
153
222
  return {
@@ -164,6 +233,8 @@ export function registerFeatureTools({ server, baseApiUrl, apiKey, appOrigin, us
164
233
  server.tool("get_stale_safe_rollouts", "Fetches all complete safe rollouts (rolled-back or released) from the GrowthBook API", {
165
234
  limit: z.number().optional().default(100),
166
235
  offset: z.number().optional().default(0),
236
+ }, {
237
+ readOnlyHint: true,
167
238
  }, async ({ limit, offset }) => {
168
239
  try {
169
240
  const queryParams = new URLSearchParams({
@@ -193,14 +264,14 @@ export function registerFeatureTools({ server, baseApiUrl, apiKey, appOrigin, us
193
264
  });
194
265
  });
195
266
  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
- `;
267
+ ${JSON.stringify(filteredSafeRollouts, null, 2)}
268
+
269
+ Share information about the rolled-back or released safe rollout rules with the user. Safe Rollout rules are stored under
270
+ environmentSettings, keyed by environment and are within the rules array with a type of "safe-rollout". Ask the user if they
271
+ would like to remove references to the feature associated with the rolled-back or released safe rollout rules and if they do,
272
+ remove the references and associated GrowthBook code and replace the values with controlValue if the safe rollout rule is rolled-back or with the
273
+ variationValue if the safe rollout is released. In addition to the current file, you may need to update other files in the codebase.
274
+ `;
204
275
  return {
205
276
  content: [{ type: "text", text }],
206
277
  };
@@ -216,6 +287,9 @@ export function registerFeatureTools({ server, baseApiUrl, apiKey, appOrigin, us
216
287
  currentWorkingDirectory: z
217
288
  .string()
218
289
  .describe("The current working directory of the user's project"),
290
+ }, {
291
+ readOnlyHint: false,
292
+ idempotentHint: true,
219
293
  }, async ({ currentWorkingDirectory }) => {
220
294
  function runCommand(command, cwd) {
221
295
  return new Promise((resolve, reject) => {
@@ -4,14 +4,23 @@ 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
+ }, {
14
+ readOnlyHint: true,
15
+ }, async ({ limit, offset, project }) => {
10
16
  try {
11
17
  const queryParams = new URLSearchParams({
12
18
  limit: limit?.toString(),
13
19
  offset: offset?.toString(),
14
20
  });
21
+ if (project) {
22
+ queryParams.append("projectId", project);
23
+ }
15
24
  const metricsRes = await fetch(`${baseApiUrl}/api/v1/metrics?${queryParams.toString()}`, {
16
25
  headers: {
17
26
  Authorization: `Bearer ${apiKey}`,
@@ -47,6 +56,8 @@ export function registerMetricsTools({ server, baseApiUrl, apiKey, appOrigin, })
47
56
  */
48
57
  server.tool("get_metric", "Fetches a metric from the GrowthBook API", {
49
58
  metricId: z.string().describe("The ID of the metric to get"),
59
+ }, {
60
+ readOnlyHint: true,
50
61
  }, async ({ metricId }) => {
51
62
  try {
52
63
  let res;
@@ -5,6 +5,8 @@ import { handleResNotOk, paginationSchema, } from "../utils.js";
5
5
  export function registerProjectTools({ server, baseApiUrl, apiKey, }) {
6
6
  server.tool("get_projects", "Fetches all projects from the GrowthBook API", {
7
7
  ...paginationSchema,
8
+ }, {
9
+ readOnlyHint: true,
8
10
  }, async ({ limit, offset }) => {
9
11
  const queryParams = new URLSearchParams({
10
12
  limit: limit.toString(),
@@ -24,9 +26,7 @@ export function registerProjectTools({ server, baseApiUrl, apiKey, }) {
24
26
  };
25
27
  }
26
28
  catch (error) {
27
- return {
28
- content: [{ type: "text", text: `Error: ${error}` }],
29
- };
29
+ throw new Error(`Error fetching projects: ${error}`);
30
30
  }
31
31
  });
32
32
  }
@@ -5,13 +5,22 @@ 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
+ }, {
14
+ readOnlyHint: true,
15
+ }, async ({ limit, offset, project }) => {
10
16
  try {
11
17
  const queryParams = new URLSearchParams({
12
18
  limit: limit?.toString(),
13
19
  offset: offset?.toString(),
14
20
  });
21
+ if (project) {
22
+ queryParams.append("projectId", project);
23
+ }
15
24
  const res = await fetch(`${baseApiUrl}/api/v1/sdk-connections?${queryParams.toString()}`, {
16
25
  headers: {
17
26
  Authorization: `Bearer ${apiKey}`,
@@ -60,12 +69,19 @@ export function registerSdkConnectionTools({ server, baseApiUrl, apiKey, }) {
60
69
  "edge-other",
61
70
  "other",
62
71
  ])
63
- .describe("The language of the SDK. Either 'javascript' or 'typescript'."),
72
+ .describe("The language or platform for the SDK connection."),
64
73
  environment: z
65
74
  .string()
66
75
  .optional()
67
76
  .describe("The environment associated with the SDK connection."),
68
- }, async ({ name, language, environment }) => {
77
+ projects: z
78
+ .array(z.string())
79
+ .describe("The projects to create the SDK connection in")
80
+ .optional(),
81
+ }, {
82
+ readOnlyHint: false,
83
+ destructiveHint: false,
84
+ }, async ({ name, language, environment, projects }) => {
69
85
  if (!environment) {
70
86
  try {
71
87
  const res = await fetch(`${baseApiUrl}/api/v1/environments`, {
@@ -94,6 +110,7 @@ export function registerSdkConnectionTools({ server, baseApiUrl, apiKey, }) {
94
110
  name,
95
111
  language,
96
112
  environment,
113
+ ...(projects && { projects }),
97
114
  };
98
115
  try {
99
116
  const res = await fetch(`${baseApiUrl}/api/v1/sdk-connections`, {
@@ -8,6 +8,8 @@ export function registerSearchTools({ server }) {
8
8
  query: z
9
9
  .string()
10
10
  .describe("The search query to look up in the GrowthBook docs."),
11
+ }, {
12
+ readOnlyHint: true,
11
13
  }, async ({ query }) => {
12
14
  const hits = await searchGrowthBookDocs(query);
13
15
  return {
@@ -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
+ };
File without changes