@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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@growthbook/mcp",
3
3
  "mcpName": "io.github.growthbook/growthbook-mcp",
4
- "version": "1.4.4",
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
- "bump:patch": "npm version patch --no-git-tag-version",
16
- "bump:minor": "npm version minor --no-git-tag-version",
17
- "bump:major": "npm version major --no-git-tag-version",
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
- "typescript": "^5.9.2"
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. You can create and manage feature flags, experiments (A/B tests), and other resources associated with GrowthBook.
29
+ instructions: `You are a helpful assistant that interacts with GrowthBook, an open source feature flagging and experimentation platform.
30
30
 
31
- **Key Workflows:**
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
- 1. **Creating Feature Flags:**
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: {},
@@ -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.tool("get_defaults", "Get the default values for experiments, including hypothesis, description, datasource, assignment query, and environments.", {}, {
253
- readOnlyHint: true,
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.tool("set_user_defaults", "Set user-defined defaults for datasource, assignment query, and environments. These will override the automatic defaults for these fields.", {
266
- datasourceId: z.string().describe("The data source ID to use as default"),
267
- assignmentQueryId: z
268
- .string()
269
- .describe("The assignment query ID to use as default"),
270
- environments: z
271
- .array(z.string())
272
- .describe("List of environment IDs to use as defaults"),
273
- }, {
274
- readOnlyHint: false,
275
- destructiveHint: true,
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.tool("clear_user_defaults", "Clear user-defined defaults and revert to automatic defaults.", {}, {
300
- readOnlyHint: false,
301
- destructiveHint: true,
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.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,
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
- // Helper functions
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