@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 +1 -122
- package/build/index.js +8 -0
- package/build/tools/environments.js +1 -3
- package/build/tools/experiments.js +59 -88
- package/build/tools/features.js +102 -43
- package/build/tools/metrics.js +94 -0
- package/build/tools/projects.js +3 -7
- package/build/tools/sdk-connections.js +17 -6
- package/build/utils.js +38 -0
- package/package.json +10 -3
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
|
-
|
|
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
|
-
|
|
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
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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(
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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(
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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: [
|
|
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
|
|
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`, {
|
package/build/tools/features.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { getDocsMetadata, handleResNotOk, generateLinkToGrowthBook,
|
|
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:
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
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
|
-
|
|
102
|
-
|
|
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:
|
|
130
|
-
|
|
131
|
-
}, async ({ id, project }) => {
|
|
192
|
+
id: featureFlagSchema.id,
|
|
193
|
+
}, async ({ id }) => {
|
|
132
194
|
try {
|
|
133
|
-
const
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
+
}
|
package/build/tools/projects.js
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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",
|