@growthbook/mcp 1.3.0 → 1.4.2

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,11 +1,15 @@
1
1
  {
2
2
  "name": "@growthbook/mcp",
3
- "version": "1.3.0",
3
+ "mcpName": "io.github.growthbook/growthbook-mcp",
4
+ "version": "1.4.2",
4
5
  "description": "MCP Server for interacting with GrowthBook",
5
6
  "access": "public",
6
7
  "homepage": "https://github.com/growthbook/growthbook-mcp",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/growthbook/growthbook-mcp"
11
+ },
7
12
  "scripts": {
8
- "test": "echo \"Error: no test specified\" && exit 1",
9
13
  "build": "tsc",
10
14
  "dev": "tsc --watch",
11
15
  "bump:patch": "npm version patch --no-git-tag-version",
package/server/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { registerEnvironmentTools } from "./tools/environments.js";
5
- import { registerExperimentTools } from "./tools/experiments.js";
5
+ import { registerExperimentTools } from "./tools/experiments/experiments.js";
6
6
  import { registerFeatureTools } from "./tools/features.js";
7
7
  import { registerProjectTools } from "./tools/projects.js";
8
8
  import { registerSdkConnectionTools } from "./tools/sdk-connections.js";
@@ -10,7 +10,8 @@ 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
+ import { registerExperimentPrompts } from "./prompts/experiment-prompts.js";
14
+ import packageDetails from "../package.json" with { type: "json" };
14
15
  export const baseApiUrl = getApiUrl();
15
16
  export const apiKey = getApiKey();
16
17
  export const appOrigin = getAppOrigin();
@@ -21,7 +22,9 @@ if (!user) {
21
22
  // Create an MCP server
22
23
  const server = new McpServer({
23
24
  name: "GrowthBook MCP",
24
- version: "1.0.2",
25
+ version: packageDetails.version,
26
+ title: "GrowthBook MCP",
27
+ websiteUrl: "https://growthbook.io",
25
28
  }, {
26
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.
27
30
 
@@ -51,6 +54,10 @@ const server = new McpServer({
51
54
  - Feature flags and experiments require a fileExtension parameter for proper code integration
52
55
  - Always review generated GrowthBook links with users so they can launch experiments
53
56
  - When experiments are "draft", users must visit GrowthBook to review and launch them`,
57
+ capabilities: {
58
+ tools: {},
59
+ prompts: {},
60
+ },
54
61
  });
55
62
  registerEnvironmentTools({
56
63
  server,
@@ -96,7 +103,7 @@ registerMetricsTools({
96
103
  appOrigin,
97
104
  user,
98
105
  });
99
- registerExperimentAnalysisPrompt({
106
+ registerExperimentPrompts({
100
107
  server,
101
108
  });
102
109
  // Start receiving messages on stdin and sending messages on stdout
@@ -0,0 +1,30 @@
1
+ export function registerExperimentPrompts({ server }) {
2
+ server.registerPrompt("experiment-analysis", {
3
+ title: "Analyze my experiment program",
4
+ description: "Analyze recent experiments and give me actionable advice",
5
+ }, () => ({
6
+ messages: [
7
+ {
8
+ role: "user",
9
+ content: {
10
+ type: "text",
11
+ 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.",
12
+ },
13
+ },
14
+ ],
15
+ }));
16
+ server.registerPrompt("wrapped", {
17
+ title: "GrowthBook Wrapped 2025",
18
+ description: "Generate a 'GrowthBook Wrapped' year-in-review for my experimentation program",
19
+ }, () => ({
20
+ messages: [
21
+ {
22
+ role: "user",
23
+ content: {
24
+ type: "text",
25
+ text: "**Role:** Expert Frontend Developer & Data Storyteller\n**Task:** Generate a 'GrowthBook Wrapped 2025' interactive React slideshow component.\n**Aesthetic:** Spotify Wrapped meets Glassmorphism — dark mode, gradient-rich, bold typography.\n\n---\n\n## Phase 0: Brand & Design Constraints\n\n### Color Palette (Use GrowthBook Brand Colors)\n| Purpose | Colors |\n|---------|--------|\n| **Primary Gradient** | `#7B45EA` (purple) → `#2076FF` (blue) → `#06B8F4` (cyan) |\n| **Wins/Success** | Emerald/Teal gradients (`from-emerald-500 to-teal-600`) |\n| **Losses/Learnings** | Rose/Amber gradients (`from-rose-500 to-amber-600`) |\n| **Neutral/Stats** | Zinc/Slate gradients (`from-zinc-800 to-slate-900`) |\n\n*Constraint:* Never use flat solid backgrounds. Always use deep, rich gradients (e.g., `bg-gradient-to-br from-indigo-950 via-purple-900 to-black`).\n\n### Typography\n| Element | Style |\n|---------|-------|\n| **Hero Metrics** | `text-7xl md:text-8xl font-black tracking-tighter` |\n| **Headlines** | `text-2xl md:text-3xl font-bold leading-tight` |\n| **Labels** | `text-xs uppercase tracking-widest text-white/60` |\n| **Body** | `text-base md:text-lg text-white/80` |\n\n**Fonts:** Import from Google Fonts:\n- Display/Metrics: `Inter` (weight 800)\n- Body: `Inter` (weights 400, 500)\n- Mono: `IBM Plex Mono` (weight 400)\n\n### Visual Motifs\n- **Glassmorphism containers:** `bg-white/10 backdrop-blur-xl border border-white/20 rounded-3xl`\n- **Glow effects:** Use `shadow-[0_0_60px_-15px_rgba(123,69,234,0.5)]` for emphasis\n\n---\n\n## Phase 1: Data Acquisition & Processing\n\n### Step 1: Fetch Data\n1. Call `get_experiments` with `{ mode: 'summary', limit: 100 }`.\n2. Call `get_projects` to map project IDs to human-readable names.\n\n*Note: This may take 30-60 seconds. Tell the user to hang tight* \n\n### Step 2: Calculate Metrics\n\n**A. Summary Stats**\n| Variable | Calculation |\n|----------|-------------|\n| `total` | `summary.total` (stopped experiments only) |\n| `drafts` | `_meta.excluded.draft` |\n| `running` | `_meta.excluded.running` |\n| `totalUsers` | `summary.totalUsers` |\n| `winRate` | `summary.winRate` (as percentage) |\n| `lossRate` | `summary.byVerdict.lost / summary.total` |\n| `inconclusiveRate` | `summary.byVerdict.inconclusive / summary.total` |\n| `avgDuration` | `summary.avgDurationDays` |\n| `medianDuration` | `summary.medianDurationDays` |\n| `srmFailureRate` | `summary.srmFailureRate` |\n| `guardrailRegressionRate` | `summary.guardrailRegressionRate` |\n\n**B. Monthly Trends**\n- Parse `summary.byMonth` keys (format: `'2025-12'`)\n- Identify **Busiest Month**: month with highest `ended` count\n- Calculate **H1 vs H2 Velocity**: sum of Jan-Jun vs Jul-Dec completions\n\n**C. Project Insights**\n- Join `summary.byProject` with project names from `get_projects`\n- Find: Project with most experiments, project with highest win rate\n- If only one project exists, skip comparative insights\n\n**D. Tag Insights**\n- If `summary.byTag` is empty → skip tag slide\n- Otherwise: most common tag, highest win-rate tag\n\n**E. Winners & Losers**\n| Variable | Source |\n|----------|--------|\n| `biggestWinner` | `topWinners[0]` (highest lift) |\n| `biggestLearning` | `topLosers[0]` (frame as 'Impact Avoided') |\n| `top3Wins` | `topWinners.slice(0, 3)` |\n\n**F. Fun Facts**\n- **Longest experiment:** Max `durationDays` from `experiments[]`\n- **Most users:** Max `totalUsers` from `experiments[]`\n- **Quickest win:** Min `durationDays` where `verdict === 'won'`\n- **SRM catches:** `srmIssues.length` (frame positively: 'GrowthBook protected your data integrity')\n\n---\n\n## Phase 2: Slide Manifest (10 Slides)\n\n| # | Slide | Key Content | Background Gradient |\n|---|-------|-------------|---------------------|\n| 1 | **Intro** | 'Your 2025 Wrapped' + GrowthBook logo | Brand purple→blue→cyan |\n| 2 | **Volume** | Total experiments + total users reached | Brand gradient |\n| 3 | **Win Rate** | Hero metric: X% win rate | Emerald/teal |\n| 4 | **Biggest Winner** | Experiment name + lift + hypothesis | Emerald/teal |\n| 5 | **Biggest Learning** | 'You avoided a -X% impact' | Rose/amber (positive framing) |\n| 6 | **Velocity** | Busiest month + H1 vs H2 comparison | Zinc/slate |\n| 7 | **Rigor** | SRM catches + guardrail saves | Zinc/slate |\n| 8 | **Fun Facts** | 2-3 memorable stats | Brand gradient |\n| 9 | **Summary** | Quick stats grid (4-6 metrics) | Brand gradient |\n| 10 | **Share Card** | Privacy-safe summary for screenshots | Holographic brand gradient |\n\n### Slide Skip Logic\n| Condition | Action |\n|-----------|--------|\n| `total === 0` | Show 'No completed experiments' single slide |\n| `total < 3` | Show simplified 5-slide version |\n| `topWinners.length === 0` | Skip slides 4, 8 |\n| `topLosers.length === 0` | Skip slide 5 |\n| `byTag` is empty | Skip any tag-related content |\n| `srmIssues.length === 0` | Simplify slide 7 |\n\n---\n\n## Phase 3: React Component Architecture\n\n### File Structure\nSingle-file React component with:\n- Framer Motion for animations\n- Tailwind CSS for styling\n- Google Fonts import in `<style>` tag\n\n### Core Components\n```tsx\n// Reusable slide wrapper\n<SlideContainer gradient='from-purple-950 via-indigo-900 to-black'>\n {children}\n</SlideContainer>\n\n// Features:\n// - Full viewport height with safe area padding (notch-friendly)\n// - Vertical centering for hero content\n// - Consistent padding: p-6 md:p-12\n// - Glass card option for content containers\n```\n\n### Loading State\nWhile data is fetching, display:\n- Animated gradient background (subtle movement)\n- Pulsing GrowthBook logo or circular spinner\n- Text: 'Crunching your 2025 experiments...' with fade animation\n- Optional: Fake progress bar (0→90% over 10s, pause until loaded)\n\n### Navigation\n- **Click/Tap:** Right half → next, Left half → previous\n- **Keyboard:** Arrow keys (← →), Spacebar (next)\n- **Swipe:** Support touch swipe gestures\n- **Progress indicator:** Dot navigation at bottom (current slide highlighted)\n\n### Animation Specs (Critical)\n\n**Directional Awareness:**\n```tsx\n// Track direction for enter/exit animations\nconst [[page, direction], setPage] = useState([0, 0]);\n\n// Variants\nconst variants = {\n enter: (dir) => ({ x: dir > 0 ? '100%' : '-100%', opacity: 0 }),\n center: { x: 0, opacity: 1 },\n exit: (dir) => ({ x: dir < 0 ? '100%' : '-100%', opacity: 0 }),\n};\n```\n\n**Physics:**\n```tsx\ntransition: { \n type: 'spring', \n stiffness: 300, \n damping: 30 \n}\n```\n\n**Staggered Entrance (per slide):**\n1. `0ms` — Background/container fades in\n2. `150ms` — Label slides up (`y: 20 → 0`)\n3. `300ms` — Hero metric pops in (`scale: 0.8 → 1`, `opacity: 0 → 1`)\n4. `450ms` — Supporting text/chart fades in\n5. `600ms` — Any secondary elements\n\n**Exit Strategy:**\n- Use `mode='popLayout'` in `<AnimatePresence>`\n- Outgoing slide exits while incoming enters (parallax crossfade)\n\n### Accessibility\n- `prefers-reduced-motion`: Disable spring animations, use simple fades\n- Semantic HTML: Use `<section>` for slides, `<h1>` for hero metrics\n- Screen reader: `aria-live='polite'` for slide changes\n\n---\n\n## Phase 4: The Share Card (Slide 10)\n\n### Purpose\nA screenshot-optimized slide users can share on social media.\n\n### Design Specs\n| Property | Value |\n|----------|-------|\n| Background | Holographic gradient: brand colors with subtle transparency |\n| Padding | 48px safe margins |\n| Contrast | High — white text on dark gradient |\n\n### Content Rules\n\n✅ **INCLUDE:**\n- Total experiments completed\n- Win rate (%)\n- Learning rate (%) — `lost / total`\n- Inconclusive rate (%)\n- Total users reached\n- Year: '2025'\n- Footer: 'Powered by GrowthBook' with SVG logo\n\n❌ **EXCLUDE (Privacy):**\n- Experiment names\n- Project names\n- Hypothesis text\n- Specific lift percentages\n- Owner/user names\n- Any identifiable business data\n\n### Logo Asset\n```svg\n<svg xmlns='http://www.w3.org/2000/svg' width='313' height='50' fill='none'><g fill-rule='evenodd' clip-path='url(#a)' clip-rule='evenodd'><path fill='#fff' d='M54.904 28.479c0 10.154 7.594 16.81 16.93 16.81 5.61 0 10.109-2.331 13.301-5.878V27.264H69.85v5.005h9.626v5.052c-1.45 1.36-4.353 2.915-7.642 2.915-6.385 0-11.125-4.956-11.125-11.758 0-6.802 4.74-11.708 11.125-11.708 1.637-.001 3.25.39 4.706 1.143a10.284 10.284 0 0 1 3.662 3.18l4.643-2.622c-2.612-3.742-6.723-6.754-13.011-6.754-9.336 0-16.93 6.608-16.93 16.762Zm34.531 16.178h5.079V28.673c1.016-1.651 3.87-3.109 5.998-3.109a7.47 7.47 0 0 1 1.596.146v-5.053c-3.048 0-5.853 1.749-7.594 3.984v-3.45h-5.079v23.466Zm25.875.584c7.497 0 12.043-5.588 12.043-12.34 0-6.706-4.546-12.293-12.043-12.293-7.401 0-11.996 5.587-11.996 12.292 0 6.753 4.595 12.34 11.996 12.34Zm0-4.518c-4.305 0-6.724-3.643-6.724-7.822 0-4.13 2.419-7.774 6.724-7.774 4.353 0 6.771 3.644 6.771 7.774 0 4.177-2.418 7.822-6.771 7.822Zm36.079 3.934h5.321l7.255-23.466h-5.272l-2.467 8.527-2.466 8.526-5.563-17.053h-4.45l-5.562 17.053-4.934-17.053h-5.273l7.255 23.466h5.322l5.417-17.198 5.417 17.198Zm21.664.584c2.37 0 3.869-.632 4.788-1.507l-1.209-3.839c-.387.438-1.306.826-2.273.826-1.452 0-2.225-1.165-2.225-2.768V25.66h4.74v-4.47h-4.74V14.78h-5.079v6.413h-3.869v4.47h3.869v13.554c0 3.887 2.08 6.024 5.998 6.024Zm22.779-.584h5.079V28.043c0-4.81-2.515-7.434-7.546-7.434-3.676 0-6.723 1.944-8.271 3.79V12.251h-5.078v32.405h5.078V28.285c1.21-1.603 3.435-3.158 5.998-3.158 2.854 0 4.74 1.117 4.74 4.761v14.77Zm10.347 0h16.301c6.045 0 9.383-3.741 9.383-8.745 0-3.983-2.806-7.433-6.24-7.967 2.999-.632 5.611-3.352 5.611-7.434 0-4.567-3.289-8.26-9.189-8.26h-15.866v32.407Zm5.659-18.996v-8.405h9.046c2.901 0 4.547 1.798 4.547 4.226 0 2.429-1.646 4.178-4.547 4.178l-9.046.001Zm0 13.992v-8.988h9.286c3.241 0 4.934 2.04 4.934 4.47 0 2.818-1.838 4.518-4.934 4.518h-9.286Zm34.627 5.587c7.498 0 12.045-5.587 12.045-12.34 0-6.704-4.547-12.291-12.045-12.291-7.4 0-11.997 5.586-11.997 12.291 0 6.753 4.597 12.34 11.997 12.34Zm0-4.517c-4.304 0-6.723-3.643-6.723-7.822 0-4.13 2.419-7.774 6.723-7.774 4.355 0 6.772 3.644 6.772 7.774 0 4.177-2.417 7.822-6.772 7.822Zm26.261 4.517c7.498 0 12.045-5.587 12.045-12.34 0-6.704-4.547-12.291-12.045-12.291-7.401 0-11.995 5.586-11.995 12.291 0 6.753 4.594 12.34 11.995 12.34Zm0-4.517c-4.305 0-6.724-3.643-6.724-7.822 0-4.13 2.419-7.774 6.724-7.774 4.354 0 6.771 3.644 6.771 7.774 0 4.177-2.417 7.822-6.771 7.822Zm31.194 3.934h6.384l-9.915-12.826 9.723-10.64h-6.287l-10.304 11.32v-20.26h-5.079v32.407h5.079v-6.316l3.24-3.352 7.159 9.667Z'/><path fill='#06B8F4' d='M16.26 17.4 46.626.242s-2.863 2.53-2.737 8.004c.134 5.835 2.737 8.004 2.737 8.004l-3.568-1.633-27.933 10.982s-.678-1.908-.69-3.346c-.028-3.549 1.825-4.851 1.825-4.851Z'/><path fill='#2076FF' d='M9.8 28.619 42.978 16.43s-2.862 2.529-2.736 8.004c.134 5.834 2.736 8.004 2.736 8.004l-4.634-1.792-29.779 5.887s-.577-1.76-.588-3.064c-.028-3.55 1.825-4.851 1.825-4.851Z'/><path fill='#7B45EA' d='m3.346 38.503 35.004-6.307s-2.863 2.53-2.737 8.004c.135 5.835 2.737 8.004 2.737 8.004H3.607s-2.055-1.254-2.086-5.032c-.029-3.55 1.825-4.67 1.825-4.67Z'/></g><defs><clipPath id='a'><path fill='#fff' d='M0 .012h312.5v49.883H0z'/></clipPath></defs></svg>\n```\n\n---\n\n## Phase 5: Date & Number Formatting\n\n### Dates\n| Context | Format | Example |\n|---------|--------|---------|\n| Monthly trends | `'MMMM yyyy'` | 'December 2025' |\n| Specific dates | `'MMM d, yyyy'` | 'Dec 11, 2025' |\n\nUse `Intl.DateTimeFormat('en-US', options)` for consistency.\n\n### Numbers\n| Type | Format | Example |\n|------|--------|---------|\n| Percentages | 0 decimal places | '60%' |\n| Users (large) | Abbreviated with suffix | '45K' or '1.2M' |\n| Users (small) | Comma-separated | '8,500' |\n| Lift | 1 decimal + sign | '+26.1%' |\n| Days | Integer | '23 days' |\n\n---\n\n## Final Checklist\n\nBefore outputting the component, verify:\n\n- [ ] All 10 slides render (or appropriate subset based on data)\n- [ ] Slide navigation works (click, keyboard, swipe)\n- [ ] Animations are smooth and directional\n- [ ] Loading state displays during data fetch\n- [ ] Edge cases handled (empty tags, single project, no winners)\n- [ ] Share card contains NO private data\n- [ ] GrowthBook logo renders correctly on dark background\n- [ ] Fonts load from Google Fonts\n- [ ] Mobile responsive (test at 375px width)\n- [ ] `prefers-reduced-motion` respected",
26
+ },
27
+ },
28
+ ],
29
+ }));
30
+ }
@@ -1,4 +1,4 @@
1
- import { handleResNotOk } from "../utils.js";
1
+ import { handleResNotOk, fetchWithRateLimit, } from "../utils.js";
2
2
  import envPaths from "env-paths";
3
3
  import { writeFile, readFile, mkdir, unlink } from "fs/promises";
4
4
  import { join } from "path";
@@ -9,7 +9,7 @@ const experimentDefaultsFile = join(experimentDefaultsDir, "experiment-defaults.
9
9
  const userDefaultsFile = join(experimentDefaultsDir, "user-defaults.json");
10
10
  export async function createDefaults(apiKey, baseApiUrl) {
11
11
  try {
12
- const experimentsResponse = await fetch(`${baseApiUrl}/api/v1/experiments`, {
12
+ const experimentsResponse = await fetchWithRateLimit(`${baseApiUrl}/api/v1/experiments`, {
13
13
  headers: {
14
14
  Authorization: `Bearer ${apiKey}`,
15
15
  },
@@ -18,7 +18,7 @@ export async function createDefaults(apiKey, baseApiUrl) {
18
18
  const experimentData = await experimentsResponse.json();
19
19
  if (experimentData.experiments.length === 0) {
20
20
  // No experiments: return assignment query and environments if possible
21
- const assignmentQueryResponse = await fetch(`${baseApiUrl}/api/v1/data-sources`, {
21
+ const assignmentQueryResponse = await fetchWithRateLimit(`${baseApiUrl}/api/v1/data-sources`, {
22
22
  headers: {
23
23
  Authorization: `Bearer ${apiKey}`,
24
24
  },
@@ -29,7 +29,7 @@ export async function createDefaults(apiKey, baseApiUrl) {
29
29
  throw new Error("No data source or assignment query found. Experiments require a data source/assignment query. Set these up in the GrowthBook and try again.");
30
30
  }
31
31
  const assignmentQuery = dataSourceData.dataSources[0].assignmentQueries[0].id;
32
- const environmentsResponse = await fetch(`${baseApiUrl}/api/v1/environments`, {
32
+ const environmentsResponse = await fetchWithRateLimit(`${baseApiUrl}/api/v1/environments`, {
33
33
  headers: {
34
34
  Authorization: `Bearer ${apiKey}`,
35
35
  },
@@ -53,7 +53,7 @@ export async function createDefaults(apiKey, baseApiUrl) {
53
53
  }
54
54
  let experiments = [];
55
55
  if (experimentData.hasMore) {
56
- const mostRecentExperiments = await fetch(`${baseApiUrl}/api/v1/experiments?offset=${experimentData.total -
56
+ const mostRecentExperiments = await fetchWithRateLimit(`${baseApiUrl}/api/v1/experiments?offset=${experimentData.total -
57
57
  Math.min(50, experimentData.count + experimentData.offset)}&limit=${Math.min(50, experimentData.count + experimentData.offset)}`, {
58
58
  headers: {
59
59
  Authorization: `Bearer ${apiKey}`,
@@ -112,7 +112,7 @@ export async function createDefaults(apiKey, baseApiUrl) {
112
112
  }
113
113
  }
114
114
  // Fetch environments
115
- const environmentsResponse = await fetch(`${baseApiUrl}/api/v1/environments`, {
115
+ const environmentsResponse = await fetchWithRateLimit(`${baseApiUrl}/api/v1/environments`, {
116
116
  headers: {
117
117
  Authorization: `Bearer ${apiKey}`,
118
118
  },
@@ -179,7 +179,7 @@ export async function getDefaults(apiKey, baseApiUrl) {
179
179
  // Re-generate auto defaults if expired
180
180
  const generatedDefaults = await createDefaults(apiKey, baseApiUrl);
181
181
  await mkdir(experimentDefaultsDir, { recursive: true });
182
- await writeFile(experimentDefaultsFile, JSON.stringify(generatedDefaults, null, 2));
182
+ await writeFile(experimentDefaultsFile, JSON.stringify(generatedDefaults));
183
183
  autoDefaults = {
184
184
  name: generatedDefaults.name,
185
185
  hypothesis: generatedDefaults.hypothesis,
@@ -192,7 +192,7 @@ export async function getDefaults(apiKey, baseApiUrl) {
192
192
  // Generate new auto defaults
193
193
  const generatedDefaults = await createDefaults(apiKey, baseApiUrl);
194
194
  await mkdir(experimentDefaultsDir, { recursive: true });
195
- await writeFile(experimentDefaultsFile, JSON.stringify(generatedDefaults, null, 2));
195
+ await writeFile(experimentDefaultsFile, JSON.stringify(generatedDefaults));
196
196
  autoDefaults = {
197
197
  name: generatedDefaults.name,
198
198
  hypothesis: generatedDefaults.hypothesis,
@@ -226,7 +226,7 @@ export async function getDefaults(apiKey, baseApiUrl) {
226
226
  ) {
227
227
  const generatedExperimentDefaults = await createDefaults(apiKey, baseApiUrl);
228
228
  await mkdir(experimentDefaultsDir, { recursive: true });
229
- await writeFile(experimentDefaultsFile, JSON.stringify(generatedExperimentDefaults, null, 2));
229
+ await writeFile(experimentDefaultsFile, JSON.stringify(generatedExperimentDefaults));
230
230
  parsedExperimentDefaults = generatedExperimentDefaults;
231
231
  }
232
232
  experimentDefaults = parsedExperimentDefaults;
@@ -236,7 +236,7 @@ export async function getDefaults(apiKey, baseApiUrl) {
236
236
  // experimentDefaultsFile does not exist, generate new defaults
237
237
  const generatedExperimentDefaults = await createDefaults(apiKey, baseApiUrl);
238
238
  await mkdir(experimentDefaultsDir, { recursive: true });
239
- await writeFile(experimentDefaultsFile, JSON.stringify(generatedExperimentDefaults, null, 2));
239
+ await writeFile(experimentDefaultsFile, JSON.stringify(generatedExperimentDefaults));
240
240
  experimentDefaults = generatedExperimentDefaults;
241
241
  }
242
242
  else {
@@ -257,7 +257,7 @@ export async function registerDefaultsTools({ server, baseApiUrl, apiKey, }) {
257
257
  content: [
258
258
  {
259
259
  type: "text",
260
- text: JSON.stringify(defaults, null, 2),
260
+ text: JSON.stringify(defaults),
261
261
  },
262
262
  ],
263
263
  };
@@ -282,7 +282,7 @@ export async function registerDefaultsTools({ server, baseApiUrl, apiKey, }) {
282
282
  timestamp: new Date().toISOString(),
283
283
  };
284
284
  await mkdir(experimentDefaultsDir, { recursive: true });
285
- await writeFile(userDefaultsFile, JSON.stringify(userDefaults, null, 2));
285
+ await writeFile(userDefaultsFile, JSON.stringify(userDefaults));
286
286
  return {
287
287
  content: [
288
288
  {
@@ -1,4 +1,4 @@
1
- import { handleResNotOk } from "../utils.js";
1
+ import { handleResNotOk, fetchWithRateLimit, } from "../utils.js";
2
2
  /**
3
3
  * Tool: get_environments
4
4
  */
@@ -7,7 +7,7 @@ export function registerEnvironmentTools({ server, baseApiUrl, apiKey, }) {
7
7
  readOnlyHint: true,
8
8
  }, async () => {
9
9
  try {
10
- const res = await fetch(`${baseApiUrl}/api/v1/environments`, {
10
+ const res = await fetchWithRateLimit(`${baseApiUrl}/api/v1/environments`, {
11
11
  headers: {
12
12
  Authorization: `Bearer ${apiKey}`,
13
13
  "Content-Type": "application/json",
@@ -16,7 +16,7 @@ export function registerEnvironmentTools({ server, baseApiUrl, apiKey, }) {
16
16
  await handleResNotOk(res);
17
17
  const data = await res.json();
18
18
  return {
19
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
19
+ content: [{ type: "text", text: JSON.stringify(data) }],
20
20
  };
21
21
  }
22
22
  catch (error) {