@growthbook/mcp 0.1.2 → 0.1.3
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 +3 -0
- package/build/tools/defaults.js +155 -3
- package/build/tools/experiments.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -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/tools/defaults.js
CHANGED
|
@@ -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: [
|
|
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
|
}
|
|
@@ -261,8 +261,8 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
|
|
|
261
261
|
},
|
|
262
262
|
body: JSON.stringify(flagPayload),
|
|
263
263
|
});
|
|
264
|
-
const flagData = await flagRes.json();
|
|
265
264
|
await handleResNotOk(flagRes);
|
|
265
|
+
const flagData = await flagRes.json();
|
|
266
266
|
const experimentLink = generateLinkToGrowthBook(appOrigin, "experiment", experimentData.experiment.id);
|
|
267
267
|
const flagLink = generateLinkToGrowthBook(appOrigin, "features", flagData.feature.id);
|
|
268
268
|
const { stub, docs, language } = getDocsMetadata(fileExtension);
|