@growthbook/mcp 1.4.4 → 1.5.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 +11 -5
- package/server/docs.js +88 -0
- package/server/index.js +6 -26
- package/server/tools/defaults.js +31 -16
- package/server/tools/environments.js +8 -2
- package/server/tools/experiments/experiment-summary.js +1 -135
- package/server/tools/experiments/experiments.js +183 -275
- package/server/tools/experiments/summary-logic.js +134 -0
- package/server/tools/features.js +104 -108
- package/server/tools/metrics.js +67 -76
- package/server/tools/projects.js +17 -18
- package/server/tools/sdk-connections.js +65 -65
- package/server/tools/search.js +19 -14
- package/server/utils.js +112 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@growthbook/mcp",
|
|
3
3
|
"mcpName": "io.github.growthbook/growthbook-mcp",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.5.0",
|
|
5
5
|
"description": "MCP Server for interacting with GrowthBook",
|
|
6
6
|
"access": "public",
|
|
7
7
|
"homepage": "https://github.com/growthbook/growthbook-mcp",
|
|
@@ -12,9 +12,13 @@
|
|
|
12
12
|
"scripts": {
|
|
13
13
|
"build": "tsc",
|
|
14
14
|
"dev": "tsc --watch",
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
15
|
+
"test": "vitest run",
|
|
16
|
+
"test:watch": "vitest",
|
|
17
|
+
"test:coverage": "vitest run --coverage",
|
|
18
|
+
"sync-version": "node scripts/sync-version.js",
|
|
19
|
+
"bump:patch": "npm version patch --no-git-tag-version && npm run sync-version",
|
|
20
|
+
"bump:minor": "npm version minor --no-git-tag-version && npm run sync-version",
|
|
21
|
+
"bump:major": "npm version major --no-git-tag-version && npm run sync-version",
|
|
18
22
|
"mcpb:build": "npx -y @anthropic-ai/mcpb -- pack"
|
|
19
23
|
},
|
|
20
24
|
"bin": {
|
|
@@ -39,7 +43,9 @@
|
|
|
39
43
|
},
|
|
40
44
|
"devDependencies": {
|
|
41
45
|
"@types/node": "^24.2.1",
|
|
42
|
-
"
|
|
46
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
47
|
+
"typescript": "^5.9.2",
|
|
48
|
+
"vitest": "^3.2.4"
|
|
43
49
|
},
|
|
44
50
|
"type": "module"
|
|
45
51
|
}
|
package/server/docs.js
CHANGED
|
@@ -459,6 +459,91 @@ GBFeatureResult result = gb.evalFeature('my-feature');
|
|
|
459
459
|
print('Value: \${result.value}');
|
|
460
460
|
print('On: \${result.on}');
|
|
461
461
|
print('Source: \${result.source}');
|
|
462
|
+
\`\`\``,
|
|
463
|
+
rust: `## Rust Feature Flag Implementation
|
|
464
|
+
|
|
465
|
+
### Basic Feature Checks
|
|
466
|
+
Use \`is_on()\` and \`is_off()\` for simple boolean checks:
|
|
467
|
+
\`\`\`rust
|
|
468
|
+
use growthbook_rust::client::GrowthBookClientBuilder;
|
|
469
|
+
|
|
470
|
+
#[tokio::main]
|
|
471
|
+
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
472
|
+
let client = GrowthBookClientBuilder::new()
|
|
473
|
+
.api_url("https://cdn.growthbook.io".to_string())
|
|
474
|
+
.client_key("sdk-abc123".to_string())
|
|
475
|
+
.build()
|
|
476
|
+
.await?;
|
|
477
|
+
|
|
478
|
+
// Simple boolean check
|
|
479
|
+
if client.is_on("my-feature", None) {
|
|
480
|
+
println!("Feature is enabled!");
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Check if feature is disabled
|
|
484
|
+
if client.is_off("maintenance-mode", None) {
|
|
485
|
+
println!("Service is operational");
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
Ok(())
|
|
489
|
+
}
|
|
490
|
+
\`\`\`
|
|
491
|
+
|
|
492
|
+
### Feature Values with Type Safety
|
|
493
|
+
Use \`feature_result()\` and \`value_as::<T>()\` for typed values:
|
|
494
|
+
\`\`\`rust
|
|
495
|
+
// Get typed feature value
|
|
496
|
+
let result = client.feature_result("button-color", None);
|
|
497
|
+
if let Ok(color) = result.value_as::<String>() {
|
|
498
|
+
println!("Button color: {}", color);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// With default fallback
|
|
502
|
+
let timeout = client.feature_result("api-timeout", None)
|
|
503
|
+
.value_as::<i32>()
|
|
504
|
+
.unwrap_or(30);
|
|
505
|
+
\`\`\`
|
|
506
|
+
|
|
507
|
+
### Attributes for Targeting
|
|
508
|
+
Use attributes for user-specific feature evaluation:
|
|
509
|
+
\`\`\`rust
|
|
510
|
+
use growthbook_rust::model_public::{GrowthBookAttribute, GrowthBookAttributeValue};
|
|
511
|
+
|
|
512
|
+
let mut attrs = Vec::new();
|
|
513
|
+
attrs.push(GrowthBookAttribute::new(
|
|
514
|
+
"userId".to_string(),
|
|
515
|
+
GrowthBookAttributeValue::String("user-123".to_string())
|
|
516
|
+
));
|
|
517
|
+
attrs.push(GrowthBookAttribute::new(
|
|
518
|
+
"plan".to_string(),
|
|
519
|
+
GrowthBookAttributeValue::String("premium".to_string())
|
|
520
|
+
));
|
|
521
|
+
|
|
522
|
+
if client.is_on("premium-feature", Some(attrs)) {
|
|
523
|
+
println!("Premium feature enabled for this user!");
|
|
524
|
+
}
|
|
525
|
+
\`\`\`
|
|
526
|
+
|
|
527
|
+
### Detailed Feature Evaluation
|
|
528
|
+
Use \`feature_result()\` for comprehensive feature information:
|
|
529
|
+
\`\`\`rust
|
|
530
|
+
let result = client.feature_result("my-feature", None);
|
|
531
|
+
|
|
532
|
+
// Access the raw value
|
|
533
|
+
println!("Value: {:?}", result.value);
|
|
534
|
+
|
|
535
|
+
// Check if enabled
|
|
536
|
+
if result.on {
|
|
537
|
+
println!("Feature is on");
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Understand why this value was assigned
|
|
541
|
+
match result.source {
|
|
542
|
+
FeatureResultSource::DefaultValue => println!("Using default value"),
|
|
543
|
+
FeatureResultSource::Force => println!("Forced value from targeting rule"),
|
|
544
|
+
FeatureResultSource::Experiment => println!("Value from A/B test"),
|
|
545
|
+
FeatureResultSource::UnknownFeature => println!("Feature not found"),
|
|
546
|
+
}
|
|
462
547
|
\`\`\``,
|
|
463
548
|
};
|
|
464
549
|
export function getFeatureFlagDocs(language) {
|
|
@@ -502,6 +587,9 @@ export function getFeatureFlagDocs(language) {
|
|
|
502
587
|
case "flutter":
|
|
503
588
|
case "dart":
|
|
504
589
|
return FEATURE_FLAG_DOCS.flutter;
|
|
590
|
+
case "rust":
|
|
591
|
+
case "rs":
|
|
592
|
+
return FEATURE_FLAG_DOCS.rust;
|
|
505
593
|
default:
|
|
506
594
|
return "Feature flag documentation not available for this language. Check GrowthBook docs for implementation details.";
|
|
507
595
|
}
|
package/server/index.js
CHANGED
|
@@ -26,34 +26,14 @@ const server = new McpServer({
|
|
|
26
26
|
title: "GrowthBook MCP",
|
|
27
27
|
websiteUrl: "https://growthbook.io",
|
|
28
28
|
}, {
|
|
29
|
-
instructions: `You are a helpful assistant that interacts with GrowthBook, an open source feature flagging and experimentation platform.
|
|
29
|
+
instructions: `You are a helpful assistant that interacts with GrowthBook, an open source feature flagging and experimentation platform.
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
Key workflows:
|
|
32
|
+
- Feature flags: Use create_feature_flag for simple flags, then create_force_rule to add targeting conditions
|
|
33
|
+
- Experiments: ALWAYS call get_defaults first, then create_experiment. Experiments are created as "draft" - users must launch in GrowthBook UI
|
|
34
|
+
- Analysis: Use get_experiments with mode="summary" for quick insights
|
|
32
35
|
|
|
33
|
-
|
|
34
|
-
- Use create_feature_flag for simple boolean/string/number/json flags
|
|
35
|
-
- Use create_force_rule to add conditional rules to existing flags
|
|
36
|
-
- Always specify the correct fileExtension for code integration
|
|
37
|
-
|
|
38
|
-
2. **Creating Experiments (A/B Tests):**
|
|
39
|
-
- CRITICAL: Always call get_defaults FIRST to see naming conventions and examples
|
|
40
|
-
- Use create_experiment to create experiments
|
|
41
|
-
- Experiments automatically create linked feature flags
|
|
42
|
-
|
|
43
|
-
3. **Exploring Existing Resources:**
|
|
44
|
-
- Use get_projects, get_environments, get_feature_flags, get_experiments, or get_attributes to understand current setup
|
|
45
|
-
- Use get_single_feature_flag for detailed flag information
|
|
46
|
-
- Use get_stale_safe_rollouts to find completed rollouts that can be cleaned up
|
|
47
|
-
|
|
48
|
-
4. **SDK Integration:**
|
|
49
|
-
- Use get_sdk_connections to see existing integrations
|
|
50
|
-
- Use create_sdk_connection for new app integrations
|
|
51
|
-
- Use generate_flag_types to create TypeScript definitions
|
|
52
|
-
|
|
53
|
-
**Important Notes:**
|
|
54
|
-
- Feature flags and experiments require a fileExtension parameter for proper code integration
|
|
55
|
-
- Always review generated GrowthBook links with users so they can launch experiments
|
|
56
|
-
- When experiments are "draft", users must visit GrowthBook to review and launch them`,
|
|
36
|
+
All mutating tools require a fileExtension parameter for SDK integration guidance.`,
|
|
57
37
|
capabilities: {
|
|
58
38
|
tools: {},
|
|
59
39
|
prompts: {},
|
package/server/tools/defaults.js
CHANGED
|
@@ -249,8 +249,13 @@ export async function getDefaults(apiKey, baseApiUrl) {
|
|
|
249
249
|
* Tool: get_defaults
|
|
250
250
|
*/
|
|
251
251
|
export async function registerDefaultsTools({ server, baseApiUrl, apiKey, }) {
|
|
252
|
-
server.
|
|
253
|
-
|
|
252
|
+
server.registerTool("get_defaults", {
|
|
253
|
+
title: "Get Defaults",
|
|
254
|
+
description: "Retrieves default configuration and naming examples for creating experiments. Analyzes your existing experiments to extract patterns and identifies your most common datasource/assignment query. Always call this before create_experiment - the examples help ensure new experiments follow your organization's conventions. Returns example names, hypotheses, descriptions from existing experiments, default datasource and assignment query IDs, and available environments. User-defined defaults (from set_user_defaults) override automatic detection.",
|
|
255
|
+
inputSchema: z.object({}),
|
|
256
|
+
annotations: {
|
|
257
|
+
readOnlyHint: true,
|
|
258
|
+
},
|
|
254
259
|
}, async () => {
|
|
255
260
|
const defaults = await getDefaults(apiKey, baseApiUrl);
|
|
256
261
|
return {
|
|
@@ -262,17 +267,22 @@ export async function registerDefaultsTools({ server, baseApiUrl, apiKey, }) {
|
|
|
262
267
|
],
|
|
263
268
|
};
|
|
264
269
|
});
|
|
265
|
-
server.
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
.describe("The
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
270
|
+
server.registerTool("set_user_defaults", {
|
|
271
|
+
title: "Set User Defaults",
|
|
272
|
+
description: "Sets custom default values for experiment configuration that override automatic detection. Use when automatic defaults select the wrong datasource or assignment query. Find valid IDs by calling get_defaults first. Persists until cleared with clear_user_defaults.",
|
|
273
|
+
inputSchema: z.object({
|
|
274
|
+
datasourceId: z.string().describe("The data source ID to use as default"),
|
|
275
|
+
assignmentQueryId: z
|
|
276
|
+
.string()
|
|
277
|
+
.describe("The assignment query ID to use as default"),
|
|
278
|
+
environments: z
|
|
279
|
+
.array(z.string())
|
|
280
|
+
.describe("List of environment IDs to use as defaults"),
|
|
281
|
+
}),
|
|
282
|
+
annotations: {
|
|
283
|
+
readOnlyHint: false,
|
|
284
|
+
destructiveHint: true,
|
|
285
|
+
},
|
|
276
286
|
}, async ({ datasourceId, assignmentQueryId, environments }) => {
|
|
277
287
|
try {
|
|
278
288
|
const userDefaults = {
|
|
@@ -296,9 +306,14 @@ export async function registerDefaultsTools({ server, baseApiUrl, apiKey, }) {
|
|
|
296
306
|
throw new Error(`Error setting user defaults: ${error}`);
|
|
297
307
|
}
|
|
298
308
|
});
|
|
299
|
-
server.
|
|
300
|
-
|
|
301
|
-
|
|
309
|
+
server.registerTool("clear_user_defaults", {
|
|
310
|
+
title: "Clear User Defaults",
|
|
311
|
+
description: "Clear user-defined defaults and revert to automatic defaults.",
|
|
312
|
+
inputSchema: z.object({}),
|
|
313
|
+
annotations: {
|
|
314
|
+
readOnlyHint: false,
|
|
315
|
+
destructiveHint: true,
|
|
316
|
+
},
|
|
302
317
|
}, async () => {
|
|
303
318
|
try {
|
|
304
319
|
await readFile(userDefaultsFile, "utf8");
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { handleResNotOk, fetchWithRateLimit, } from "../utils.js";
|
|
2
|
+
import { z } from "zod";
|
|
2
3
|
/**
|
|
3
4
|
* Tool: get_environments
|
|
4
5
|
*/
|
|
5
6
|
export function registerEnvironmentTools({ server, baseApiUrl, apiKey, }) {
|
|
6
|
-
server.
|
|
7
|
-
|
|
7
|
+
server.registerTool("get_environments", {
|
|
8
|
+
title: "Get Environments",
|
|
9
|
+
description: "Lists all environments configured in GrowthBook. 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. Use this to see available environments before creating SDK connections or configuring feature flags. Environments can be scoped to specific projects for further control.",
|
|
10
|
+
inputSchema: z.object({}),
|
|
11
|
+
annotations: {
|
|
12
|
+
readOnlyHint: true,
|
|
13
|
+
},
|
|
8
14
|
}, async () => {
|
|
9
15
|
try {
|
|
10
16
|
const res = await fetchWithRateLimit(`${baseApiUrl}/api/v1/environments`, {
|
|
@@ -1,139 +1,5 @@
|
|
|
1
1
|
import { fetchWithRateLimit, handleResNotOk } from "../../utils.js";
|
|
2
|
-
|
|
3
|
-
function median(arr) {
|
|
4
|
-
if (arr.length === 0)
|
|
5
|
-
return null;
|
|
6
|
-
const sorted = [...arr].sort((a, b) => a - b);
|
|
7
|
-
const mid = Math.floor(sorted.length / 2);
|
|
8
|
-
return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
9
|
-
}
|
|
10
|
-
function round(n, decimals = 4) {
|
|
11
|
-
if (n === null || n === undefined || isNaN(n))
|
|
12
|
-
return null;
|
|
13
|
-
return Math.round(n * 10 ** decimals) / 10 ** decimals;
|
|
14
|
-
}
|
|
15
|
-
function formatLift(lift) {
|
|
16
|
-
if (lift === null)
|
|
17
|
-
return "N/A";
|
|
18
|
-
const sign = lift >= 0 ? "+" : "";
|
|
19
|
-
return `${sign}${(lift * 100).toFixed(1)}%`;
|
|
20
|
-
}
|
|
21
|
-
function getYearMonth(dateStr) {
|
|
22
|
-
if (!dateStr)
|
|
23
|
-
return null;
|
|
24
|
-
const d = new Date(dateStr);
|
|
25
|
-
if (isNaN(d.getTime()))
|
|
26
|
-
return null;
|
|
27
|
-
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`;
|
|
28
|
-
}
|
|
29
|
-
function computeVerdict(exp, metricLookup) {
|
|
30
|
-
const resultData = exp.result?.results?.[0];
|
|
31
|
-
const srmPValue = resultData?.checks?.srm ?? null;
|
|
32
|
-
const totalUsers = resultData?.totalUsers || 0;
|
|
33
|
-
const srmPassing = srmPValue !== null ? srmPValue > 0.001 : true;
|
|
34
|
-
// Get goal and guardrail metric IDs
|
|
35
|
-
const goalIds = exp.settings?.goals?.map((g) => g.metricId) || [];
|
|
36
|
-
const guardrailIds = new Set(exp.settings?.guardrails?.map((g) => g.metricId) || []);
|
|
37
|
-
// Check guardrail regression
|
|
38
|
-
const guardrailsRegressed = resultData
|
|
39
|
-
? (resultData.metrics || [])
|
|
40
|
-
.filter((m) => guardrailIds.has(m.metricId))
|
|
41
|
-
.some((m) => {
|
|
42
|
-
const metricInfo = metricLookup.get(m.metricId);
|
|
43
|
-
const isInverse = metricInfo?.inverse ?? false;
|
|
44
|
-
return m.variations.slice(1).some((v) => {
|
|
45
|
-
const analysis = v.analyses?.[0];
|
|
46
|
-
if (!analysis)
|
|
47
|
-
return false;
|
|
48
|
-
if (analysis.chanceToBeatControl !== undefined) {
|
|
49
|
-
return isInverse
|
|
50
|
-
? analysis.chanceToBeatControl > 0.95
|
|
51
|
-
: analysis.chanceToBeatControl < 0.05;
|
|
52
|
-
}
|
|
53
|
-
return isInverse
|
|
54
|
-
? (analysis.ciLow ?? 0) > 0
|
|
55
|
-
: (analysis.ciHigh ?? 0) < 0;
|
|
56
|
-
});
|
|
57
|
-
})
|
|
58
|
-
: false;
|
|
59
|
-
// Verdict: Match GrowthBook's ExperimentWinRate.tsx exactly
|
|
60
|
-
// exp.results maps to resultSummary.status in the API
|
|
61
|
-
const userResult = exp.resultSummary?.status?.toLowerCase() || "";
|
|
62
|
-
let verdict;
|
|
63
|
-
if (userResult === "won") {
|
|
64
|
-
verdict = "won";
|
|
65
|
-
}
|
|
66
|
-
else if (userResult === "lost") {
|
|
67
|
-
verdict = "lost";
|
|
68
|
-
}
|
|
69
|
-
else {
|
|
70
|
-
// Everything else is "inconclusive": dnf, inconclusive, undefined, null, ""
|
|
71
|
-
verdict = "inconclusive";
|
|
72
|
-
}
|
|
73
|
-
// Compute primary metric result for display
|
|
74
|
-
const primaryMetricResult = resultData
|
|
75
|
-
? computePrimaryMetricResult(resultData, metricLookup, goalIds)
|
|
76
|
-
: null;
|
|
77
|
-
return {
|
|
78
|
-
verdict,
|
|
79
|
-
primaryMetricResult,
|
|
80
|
-
guardrailsRegressed,
|
|
81
|
-
srmPassing,
|
|
82
|
-
srmPValue,
|
|
83
|
-
totalUsers,
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
function computePrimaryMetricResult(resultData, metricLookup, goalIds) {
|
|
87
|
-
const primaryMetricId = goalIds[0];
|
|
88
|
-
if (!primaryMetricId)
|
|
89
|
-
return null;
|
|
90
|
-
const primaryMetricData = resultData.metrics?.find((m) => m.metricId === primaryMetricId);
|
|
91
|
-
if (!primaryMetricData || primaryMetricData.variations.length <= 1) {
|
|
92
|
-
return null;
|
|
93
|
-
}
|
|
94
|
-
const metricInfo = metricLookup.get(primaryMetricId);
|
|
95
|
-
const isInverse = metricInfo?.inverse ?? false;
|
|
96
|
-
// Find best performing variation (excluding control at index 0)
|
|
97
|
-
let bestVariation = primaryMetricData.variations[1];
|
|
98
|
-
let bestLift = bestVariation?.analyses?.[0]?.percentChange ?? 0;
|
|
99
|
-
for (let i = 2; i < primaryMetricData.variations.length; i++) {
|
|
100
|
-
const v = primaryMetricData.variations[i];
|
|
101
|
-
const lift = v.analyses?.[0]?.percentChange ?? 0;
|
|
102
|
-
const isBetter = isInverse ? lift < bestLift : lift > bestLift;
|
|
103
|
-
if (isBetter) {
|
|
104
|
-
bestVariation = v;
|
|
105
|
-
bestLift = lift;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
const analysis = bestVariation?.analyses?.[0];
|
|
109
|
-
if (!analysis)
|
|
110
|
-
return null;
|
|
111
|
-
const lift = analysis.percentChange;
|
|
112
|
-
const chanceToBeatControl = analysis.chanceToBeatControl;
|
|
113
|
-
let significant = false;
|
|
114
|
-
if (chanceToBeatControl !== undefined) {
|
|
115
|
-
significant = chanceToBeatControl > 0.95 || chanceToBeatControl < 0.05;
|
|
116
|
-
}
|
|
117
|
-
else {
|
|
118
|
-
significant =
|
|
119
|
-
analysis.ciLow !== undefined &&
|
|
120
|
-
analysis.ciHigh !== undefined &&
|
|
121
|
-
(analysis.ciLow > 0 || analysis.ciHigh < 0);
|
|
122
|
-
}
|
|
123
|
-
let direction = "flat";
|
|
124
|
-
if (significant) {
|
|
125
|
-
const rawPositive = lift > 0;
|
|
126
|
-
const isWinning = isInverse ? !rawPositive : rawPositive;
|
|
127
|
-
direction = isWinning ? "winning" : "losing";
|
|
128
|
-
}
|
|
129
|
-
return {
|
|
130
|
-
id: primaryMetricId,
|
|
131
|
-
name: metricInfo?.name || primaryMetricId,
|
|
132
|
-
lift: round(lift),
|
|
133
|
-
significant,
|
|
134
|
-
direction,
|
|
135
|
-
};
|
|
136
|
-
}
|
|
2
|
+
import { computeVerdict, formatLift, getYearMonth, median, round, } from "./summary-logic.js";
|
|
137
3
|
// Metric Lookup with caching
|
|
138
4
|
const metricCache = new Map();
|
|
139
5
|
const CACHE_TTL_MS = 10 * 60 * 1000; // 10 minutes
|