@growthbook/mcp 1.2.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 +7 -7
- package/{build → server}/index.js +11 -1
- package/server/prompts/experiment-analysis.js +13 -0
- package/{build → server}/tools/defaults.js +10 -2
- package/{build → server}/tools/environments.js +3 -1
- package/{build → server}/tools/experiments.js +71 -3
- package/{build → server}/tools/features.js +14 -0
- package/{build → server}/tools/metrics.js +4 -0
- package/{build → server}/tools/projects.js +2 -0
- package/{build → server}/tools/sdk-connections.js +5 -0
- package/{build → server}/tools/search.js +2 -0
- /package/{build → server}/docs.js +0 -0
- /package/{build → server}/utils.js +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@growthbook/mcp",
|
|
3
|
-
"version": "1.
|
|
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",
|
|
@@ -8,15 +8,16 @@
|
|
|
8
8
|
"test": "echo \"Error: no test specified\" && exit 1",
|
|
9
9
|
"build": "tsc",
|
|
10
10
|
"dev": "tsc --watch",
|
|
11
|
-
"bump:patch": "
|
|
12
|
-
"bump:minor": "
|
|
13
|
-
"bump:major": "
|
|
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"
|
|
14
15
|
},
|
|
15
16
|
"bin": {
|
|
16
|
-
"mcp": "
|
|
17
|
+
"mcp": "server/index.js"
|
|
17
18
|
},
|
|
18
19
|
"files": [
|
|
19
|
-
"
|
|
20
|
+
"server"
|
|
20
21
|
],
|
|
21
22
|
"keywords": [
|
|
22
23
|
"growthbook",
|
|
@@ -27,7 +28,6 @@
|
|
|
27
28
|
],
|
|
28
29
|
"author": "GrowthBook",
|
|
29
30
|
"license": "MIT",
|
|
30
|
-
"packageManager": "pnpm@10.6.1",
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@modelcontextprotocol/sdk": "^1.17.2",
|
|
33
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
|
-
|
|
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.", {},
|
|
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.", {},
|
|
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.", {},
|
|
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: {
|
|
@@ -10,8 +10,14 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
|
|
|
10
10
|
.string()
|
|
11
11
|
.describe("The ID of the project to filter experiments by")
|
|
12
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."),
|
|
13
17
|
...paginationSchema,
|
|
14
|
-
},
|
|
18
|
+
}, {
|
|
19
|
+
readOnlyHint: true,
|
|
20
|
+
}, async ({ limit, offset, mostRecent, project, mode }) => {
|
|
15
21
|
try {
|
|
16
22
|
// Default behavior
|
|
17
23
|
if (!mostRecent || offset > 0) {
|
|
@@ -30,6 +36,24 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
|
|
|
30
36
|
});
|
|
31
37
|
await handleResNotOk(defaultRes);
|
|
32
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
|
+
}
|
|
33
57
|
return {
|
|
34
58
|
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
35
59
|
};
|
|
@@ -61,6 +85,23 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
|
|
|
61
85
|
if (mostRecentData.experiments &&
|
|
62
86
|
Array.isArray(mostRecentData.experiments)) {
|
|
63
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
|
+
}
|
|
64
105
|
}
|
|
65
106
|
return {
|
|
66
107
|
content: [
|
|
@@ -77,7 +118,13 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
|
|
|
77
118
|
*/
|
|
78
119
|
server.tool("get_experiment", "Gets a single experiment from GrowthBook", {
|
|
79
120
|
experimentId: z.string().describe("The ID of the experiment to get"),
|
|
80
|
-
|
|
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 }) => {
|
|
81
128
|
try {
|
|
82
129
|
const res = await fetch(`${baseApiUrl}/api/v1/experiments/${experimentId}`, {
|
|
83
130
|
headers: {
|
|
@@ -87,6 +134,22 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
|
|
|
87
134
|
});
|
|
88
135
|
await handleResNotOk(res);
|
|
89
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
|
+
}
|
|
90
153
|
const linkToGrowthBook = generateLinkToGrowthBook(appOrigin, "experiment", experimentId);
|
|
91
154
|
const text = `
|
|
92
155
|
${JSON.stringify(data, null, 2)}
|
|
@@ -104,7 +167,9 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
|
|
|
104
167
|
/**
|
|
105
168
|
* Tool: get_attributes
|
|
106
169
|
*/
|
|
107
|
-
server.tool("get_attributes", "Get all attributes", {},
|
|
170
|
+
server.tool("get_attributes", "Get all attributes", {}, {
|
|
171
|
+
readOnlyHint: true,
|
|
172
|
+
}, async () => {
|
|
108
173
|
try {
|
|
109
174
|
const queryParams = new URLSearchParams();
|
|
110
175
|
queryParams.append("limit", "100");
|
|
@@ -161,6 +226,9 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
|
|
|
161
226
|
confirmedDefaultsReviewed: z
|
|
162
227
|
.boolean()
|
|
163
228
|
.describe("Set to true to confirm you have called get_defaults and reviewed the output to guide these parameters."),
|
|
229
|
+
}, {
|
|
230
|
+
readOnlyHint: false,
|
|
231
|
+
destructiveHint: false,
|
|
164
232
|
}, async ({ description, hypothesis, name, variations, fileExtension, confirmedDefaultsReviewed, project, }) => {
|
|
165
233
|
if (!confirmedDefaultsReviewed) {
|
|
166
234
|
return {
|
|
@@ -13,6 +13,9 @@ export function registerFeatureTools({ server, baseApiUrl, apiKey, appOrigin, us
|
|
|
13
13
|
description: featureFlagSchema.description.optional().default(""),
|
|
14
14
|
project: featureFlagSchema.project.optional(),
|
|
15
15
|
fileExtension: featureFlagSchema.fileExtension,
|
|
16
|
+
}, {
|
|
17
|
+
readOnlyHint: false,
|
|
18
|
+
destructiveHint: false,
|
|
16
19
|
}, async ({ id, valueType, defaultValue, description, project, fileExtension, }) => {
|
|
17
20
|
// get environments
|
|
18
21
|
let environments = [];
|
|
@@ -97,6 +100,8 @@ export function registerFeatureTools({ server, baseApiUrl, apiKey, appOrigin, us
|
|
|
97
100
|
value: z
|
|
98
101
|
.string()
|
|
99
102
|
.describe("The type of the value should match the feature type"),
|
|
103
|
+
}, {
|
|
104
|
+
readOnlyHint: false,
|
|
100
105
|
}, async ({ featureId, description, condition, value, fileExtension }) => {
|
|
101
106
|
try {
|
|
102
107
|
// Fetch feature defaults first and surface to user
|
|
@@ -160,6 +165,8 @@ export function registerFeatureTools({ server, baseApiUrl, apiKey, appOrigin, us
|
|
|
160
165
|
server.tool("get_feature_flags", "Fetches all feature flags from the GrowthBook API, with optional limit, offset, and project filtering.", {
|
|
161
166
|
project: featureFlagSchema.project.optional(),
|
|
162
167
|
...paginationSchema,
|
|
168
|
+
}, {
|
|
169
|
+
readOnlyHint: true,
|
|
163
170
|
}, async ({ limit, offset, project }) => {
|
|
164
171
|
try {
|
|
165
172
|
const queryParams = new URLSearchParams({
|
|
@@ -190,6 +197,8 @@ export function registerFeatureTools({ server, baseApiUrl, apiKey, appOrigin, us
|
|
|
190
197
|
*/
|
|
191
198
|
server.tool("get_single_feature_flag", "Fetches a specific feature flag from the GrowthBook API", {
|
|
192
199
|
id: featureFlagSchema.id,
|
|
200
|
+
}, {
|
|
201
|
+
readOnlyHint: true,
|
|
193
202
|
}, async ({ id }) => {
|
|
194
203
|
try {
|
|
195
204
|
const res = await fetch(`${baseApiUrl}/api/v1/features/${id}`, {
|
|
@@ -224,6 +233,8 @@ export function registerFeatureTools({ server, baseApiUrl, apiKey, appOrigin, us
|
|
|
224
233
|
server.tool("get_stale_safe_rollouts", "Fetches all complete safe rollouts (rolled-back or released) from the GrowthBook API", {
|
|
225
234
|
limit: z.number().optional().default(100),
|
|
226
235
|
offset: z.number().optional().default(0),
|
|
236
|
+
}, {
|
|
237
|
+
readOnlyHint: true,
|
|
227
238
|
}, async ({ limit, offset }) => {
|
|
228
239
|
try {
|
|
229
240
|
const queryParams = new URLSearchParams({
|
|
@@ -276,6 +287,9 @@ export function registerFeatureTools({ server, baseApiUrl, apiKey, appOrigin, us
|
|
|
276
287
|
currentWorkingDirectory: z
|
|
277
288
|
.string()
|
|
278
289
|
.describe("The current working directory of the user's project"),
|
|
290
|
+
}, {
|
|
291
|
+
readOnlyHint: false,
|
|
292
|
+
idempotentHint: true,
|
|
279
293
|
}, async ({ currentWorkingDirectory }) => {
|
|
280
294
|
function runCommand(command, cwd) {
|
|
281
295
|
return new Promise((resolve, reject) => {
|
|
@@ -10,6 +10,8 @@ export function registerMetricsTools({ server, baseApiUrl, apiKey, appOrigin, })
|
|
|
10
10
|
.describe("The ID of the project to filter metrics by")
|
|
11
11
|
.optional(),
|
|
12
12
|
...paginationSchema,
|
|
13
|
+
}, {
|
|
14
|
+
readOnlyHint: true,
|
|
13
15
|
}, async ({ limit, offset, project }) => {
|
|
14
16
|
try {
|
|
15
17
|
const queryParams = new URLSearchParams({
|
|
@@ -54,6 +56,8 @@ export function registerMetricsTools({ server, baseApiUrl, apiKey, appOrigin, })
|
|
|
54
56
|
*/
|
|
55
57
|
server.tool("get_metric", "Fetches a metric from the GrowthBook API", {
|
|
56
58
|
metricId: z.string().describe("The ID of the metric to get"),
|
|
59
|
+
}, {
|
|
60
|
+
readOnlyHint: true,
|
|
57
61
|
}, async ({ metricId }) => {
|
|
58
62
|
try {
|
|
59
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(),
|
|
@@ -10,6 +10,8 @@ export function registerSdkConnectionTools({ server, baseApiUrl, apiKey, }) {
|
|
|
10
10
|
.describe("The ID of the project to filter SDK connections by")
|
|
11
11
|
.optional(),
|
|
12
12
|
...paginationSchema,
|
|
13
|
+
}, {
|
|
14
|
+
readOnlyHint: true,
|
|
13
15
|
}, async ({ limit, offset, project }) => {
|
|
14
16
|
try {
|
|
15
17
|
const queryParams = new URLSearchParams({
|
|
@@ -76,6 +78,9 @@ export function registerSdkConnectionTools({ server, baseApiUrl, apiKey, }) {
|
|
|
76
78
|
.array(z.string())
|
|
77
79
|
.describe("The projects to create the SDK connection in")
|
|
78
80
|
.optional(),
|
|
81
|
+
}, {
|
|
82
|
+
readOnlyHint: false,
|
|
83
|
+
destructiveHint: false,
|
|
79
84
|
}, async ({ name, language, environment, projects }) => {
|
|
80
85
|
if (!environment) {
|
|
81
86
|
try {
|
|
@@ -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 {
|
|
File without changes
|
|
File without changes
|