@growthbook/mcp 1.3.1 → 1.4.3

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.
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
- import { generateLinkToGrowthBook, getDocsMetadata, handleResNotOk, SUPPORTED_FILE_EXTENSIONS, paginationSchema, } from "../utils.js";
3
- import { getDefaults } from "./defaults.js";
2
+ import { generateLinkToGrowthBook, getDocsMetadata, handleResNotOk, SUPPORTED_FILE_EXTENSIONS, paginationSchema, fetchWithRateLimit, } from "../../utils.js";
3
+ import { getDefaults } from "../defaults.js";
4
+ import { handleSummaryMode } from "./experiment-summary.js";
4
5
  export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin, user, }) {
5
6
  /**
6
7
  * Tool: get_experiments
@@ -11,13 +12,29 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
11
12
  .describe("The ID of the project to filter experiments by")
12
13
  .optional(),
13
14
  mode: z
14
- .enum(["default", "analyze"])
15
- .default("default")
16
- .describe("The mode to use to fetch experiments. Default mode returns summary info about experiments. Analyze mode will also fetch experiment results, allowing for better analysis, interpretation, and reporting."),
15
+ .enum(["metadata", "summary", "full"])
16
+ .default("metadata")
17
+ .describe("The mode to use to fetch experiments. Metadata mode returns experiment config without results. Summary mode fetches results and returns pruned key stats for quick analysis. Full mode fetches and returns complete results data. WARNING: Full mode may return large payloads."),
17
18
  ...paginationSchema,
18
19
  }, {
19
20
  readOnlyHint: true,
20
- }, async ({ limit, offset, mostRecent, project, mode }) => {
21
+ }, async ({ limit, offset, mostRecent, project, mode }, extra) => {
22
+ const progressToken = extra._meta?.progressToken;
23
+ const totalSteps = mode === "summary" ? 5 : mode === "full" ? 3 : 2;
24
+ const reportProgress = async (progress, message) => {
25
+ if (progressToken) {
26
+ await server.server.notification({
27
+ method: "notifications/progress",
28
+ params: {
29
+ progressToken,
30
+ progress,
31
+ total: totalSteps,
32
+ ...(message && { message }),
33
+ },
34
+ });
35
+ }
36
+ };
37
+ await reportProgress(1, "Fetching experiments...");
21
38
  try {
22
39
  // Default behavior
23
40
  if (!mostRecent || offset > 0) {
@@ -28,7 +45,7 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
28
45
  if (project) {
29
46
  defaultQueryParams.append("projectId", project);
30
47
  }
31
- const defaultRes = await fetch(`${baseApiUrl}/api/v1/experiments?${defaultQueryParams.toString()}`, {
48
+ const defaultRes = await fetchWithRateLimit(`${baseApiUrl}/api/v1/experiments?${defaultQueryParams.toString()}`, {
32
49
  headers: {
33
50
  Authorization: `Bearer ${apiKey}`,
34
51
  "Content-Type": "application/json",
@@ -37,10 +54,15 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
37
54
  await handleResNotOk(defaultRes);
38
55
  const data = await defaultRes.json();
39
56
  const experiments = data.experiments;
40
- if (mode === "analyze") {
57
+ if (mode === "full" || mode === "summary") {
58
+ await reportProgress(2, "Fetching experiment results...");
41
59
  for (const [index, experiment] of experiments.entries()) {
60
+ if (experiment.status === "draft") {
61
+ experiments[index].result = undefined;
62
+ continue;
63
+ }
42
64
  try {
43
- const resultsRes = await fetch(`${baseApiUrl}/api/v1/experiments/${experiment.id}/results`, {
65
+ const resultsRes = await fetchWithRateLimit(`${baseApiUrl}/api/v1/experiments/${experiment.id}/results`, {
44
66
  headers: {
45
67
  Authorization: `Bearer ${apiKey}`,
46
68
  },
@@ -54,12 +76,32 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
54
76
  }
55
77
  }
56
78
  }
79
+ if (mode === "summary") {
80
+ const summaryExperiments = await handleSummaryMode(experiments, baseApiUrl, apiKey, reportProgress);
81
+ const summaryExperimentsWithPagination = {
82
+ summary: summaryExperiments,
83
+ limit: data.limit,
84
+ offset: data.offset,
85
+ total: data.total,
86
+ hasMore: data.hasMore,
87
+ nextOffset: data.nextOffset,
88
+ };
89
+ return {
90
+ content: [
91
+ {
92
+ type: "text",
93
+ text: JSON.stringify(summaryExperimentsWithPagination),
94
+ },
95
+ ],
96
+ };
97
+ }
98
+ await reportProgress(2, "Processing results...");
57
99
  return {
58
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
100
+ content: [{ type: "text", text: JSON.stringify(data) }],
59
101
  };
60
102
  }
61
103
  // Most recent behavior
62
- const countRes = await fetch(`${baseApiUrl}/api/v1/experiments?limit=1`, {
104
+ const countRes = await fetchWithRateLimit(`${baseApiUrl}/api/v1/experiments?limit=1`, {
63
105
  headers: {
64
106
  Authorization: `Bearer ${apiKey}`,
65
107
  },
@@ -75,7 +117,7 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
75
117
  if (project) {
76
118
  mostRecentQueryParams.append("projectId", project);
77
119
  }
78
- const mostRecentRes = await fetch(`${baseApiUrl}/api/v1/experiments?${mostRecentQueryParams.toString()}`, {
120
+ const mostRecentRes = await fetchWithRateLimit(`${baseApiUrl}/api/v1/experiments?${mostRecentQueryParams.toString()}`, {
79
121
  headers: {
80
122
  Authorization: `Bearer ${apiKey}`,
81
123
  },
@@ -85,10 +127,11 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
85
127
  if (mostRecentData.experiments &&
86
128
  Array.isArray(mostRecentData.experiments)) {
87
129
  mostRecentData.experiments = mostRecentData.experiments.reverse();
88
- if (mode === "analyze") {
130
+ if (mode === "full" || mode === "summary") {
131
+ await reportProgress(2, "Fetching experiment results...");
89
132
  for (const [index, experiment,] of mostRecentData.experiments.entries()) {
90
133
  try {
91
- const resultsRes = await fetch(`${baseApiUrl}/api/v1/experiments/${experiment.id}/results`, {
134
+ const resultsRes = await fetchWithRateLimit(`${baseApiUrl}/api/v1/experiments/${experiment.id}/results`, {
92
135
  headers: {
93
136
  Authorization: `Bearer ${apiKey}`,
94
137
  },
@@ -103,10 +146,30 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
103
146
  }
104
147
  }
105
148
  }
149
+ if (mode === "summary") {
150
+ const experiments = Array.isArray(mostRecentData.experiments)
151
+ ? mostRecentData.experiments
152
+ : [];
153
+ const summaryExperiments = await handleSummaryMode(experiments, baseApiUrl, apiKey, reportProgress);
154
+ const summaryExperimentsWithPagination = {
155
+ summary: summaryExperiments,
156
+ limit: mostRecentData.limit,
157
+ offset: mostRecentData.offset,
158
+ total: mostRecentData.total,
159
+ hasMore: mostRecentData.hasMore,
160
+ nextOffset: mostRecentData.nextOffset,
161
+ };
162
+ return {
163
+ content: [
164
+ {
165
+ type: "text",
166
+ text: JSON.stringify(summaryExperimentsWithPagination),
167
+ },
168
+ ],
169
+ };
170
+ }
106
171
  return {
107
- content: [
108
- { type: "text", text: JSON.stringify(mostRecentData, null, 2) },
109
- ],
172
+ content: [{ type: "text", text: JSON.stringify(mostRecentData) }],
110
173
  };
111
174
  }
112
175
  catch (error) {
@@ -119,14 +182,14 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
119
182
  server.tool("get_experiment", "Gets a single experiment from GrowthBook", {
120
183
  experimentId: z.string().describe("The ID of the experiment to get"),
121
184
  mode: z
122
- .enum(["default", "analyze"])
123
- .default("default")
124
- .describe("The mode to use to fetch the experiment. Default mode returns summary info about the experiment. Analyze mode will also fetch experiment results, allowing for better analysis, interpretation, and reporting."),
185
+ .enum(["metadata", "full"])
186
+ .default("metadata")
187
+ .describe("The mode to use to fetch the experiment. Metadata mode returns summary info about the experiment. Full mode fetches results and returns complete results data."),
125
188
  }, {
126
189
  readOnlyHint: true,
127
190
  }, async ({ experimentId, mode }) => {
128
191
  try {
129
- const res = await fetch(`${baseApiUrl}/api/v1/experiments/${experimentId}`, {
192
+ const res = await fetchWithRateLimit(`${baseApiUrl}/api/v1/experiments/${experimentId}`, {
130
193
  headers: {
131
194
  Authorization: `Bearer ${apiKey}`,
132
195
  "Content-Type": "application/json",
@@ -134,10 +197,13 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
134
197
  });
135
198
  await handleResNotOk(res);
136
199
  const data = await res.json();
137
- // If analyze mode, fetch results
138
- if (mode === "analyze") {
200
+ // If analyze or summary mode, fetch results
201
+ if (mode === "full") {
202
+ if (data.status === "draft") {
203
+ data.result = null;
204
+ }
139
205
  try {
140
- const resultsRes = await fetch(`${baseApiUrl}/api/v1/experiments/${experimentId}/results`, {
206
+ const resultsRes = await fetchWithRateLimit(`${baseApiUrl}/api/v1/experiments/${experimentId}/results`, {
141
207
  headers: {
142
208
  Authorization: `Bearer ${apiKey}`,
143
209
  },
@@ -152,7 +218,7 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
152
218
  }
153
219
  const linkToGrowthBook = generateLinkToGrowthBook(appOrigin, "experiment", experimentId);
154
220
  const text = `
155
- ${JSON.stringify(data, null, 2)}
221
+ ${JSON.stringify(data)}
156
222
 
157
223
  [View the experiment in GrowthBook](${linkToGrowthBook})
158
224
  `;
@@ -173,7 +239,7 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
173
239
  try {
174
240
  const queryParams = new URLSearchParams();
175
241
  queryParams.append("limit", "100");
176
- const res = await fetch(`${baseApiUrl}/api/v1/attributes?${queryParams.toString()}`, {
242
+ const res = await fetchWithRateLimit(`${baseApiUrl}/api/v1/attributes?${queryParams.toString()}`, {
177
243
  headers: {
178
244
  Authorization: `Bearer ${apiKey}`,
179
245
  "Content-Type": "application/json",
@@ -182,7 +248,7 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
182
248
  await handleResNotOk(res);
183
249
  const data = await res.json();
184
250
  return {
185
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
251
+ content: [{ type: "text", text: JSON.stringify(data) }],
186
252
  };
187
253
  }
188
254
  catch (error) {
@@ -258,7 +324,7 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
258
324
  ...(project && { project }),
259
325
  };
260
326
  try {
261
- const experimentRes = await fetch(`${baseApiUrl}/api/v1/experiments`, {
327
+ const experimentRes = await fetchWithRateLimit(`${baseApiUrl}/api/v1/experiments`, {
262
328
  method: "POST",
263
329
  headers: {
264
330
  Authorization: `Bearer ${apiKey}`,
@@ -298,7 +364,7 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
298
364
  }, {}),
299
365
  },
300
366
  };
301
- const flagRes = await fetch(`${baseApiUrl}/api/v1/features`, {
367
+ const flagRes = await fetchWithRateLimit(`${baseApiUrl}/api/v1/features`, {
302
368
  method: "POST",
303
369
  headers: {
304
370
  Authorization: `Bearer ${apiKey}`,
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { getDocsMetadata, handleResNotOk, generateLinkToGrowthBook, paginationSchema, featureFlagSchema, } from "../utils.js";
2
+ import { getDocsMetadata, handleResNotOk, generateLinkToGrowthBook, paginationSchema, featureFlagSchema, fetchWithRateLimit, } 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, }) {
@@ -24,7 +24,7 @@ export function registerFeatureTools({ server, baseApiUrl, apiKey, appOrigin, us
24
24
  environments = defaults.environments;
25
25
  }
26
26
  else {
27
- const envRes = await fetch(`${baseApiUrl}/api/v1/features/environments`, {
27
+ const envRes = await fetchWithRateLimit(`${baseApiUrl}/api/v1/features/environments`, {
28
28
  headers: {
29
29
  Authorization: `Bearer ${apiKey}`,
30
30
  "Content-Type": "application/json",
@@ -51,7 +51,7 @@ export function registerFeatureTools({ server, baseApiUrl, apiKey, appOrigin, us
51
51
  ...(project && { project }),
52
52
  };
53
53
  try {
54
- const res = await fetch(`${baseApiUrl}/api/v1/features`, {
54
+ const res = await fetchWithRateLimit(`${baseApiUrl}/api/v1/features`, {
55
55
  method: "POST",
56
56
  headers: {
57
57
  Authorization: `Bearer ${apiKey}`,
@@ -63,7 +63,7 @@ export function registerFeatureTools({ server, baseApiUrl, apiKey, appOrigin, us
63
63
  const data = await res.json();
64
64
  const { docs, language, stub } = getDocsMetadata(fileExtension);
65
65
  const linkToGrowthBook = generateLinkToGrowthBook(appOrigin, "features", id);
66
- const text = `This is the API response: ${JSON.stringify(data, null, 2)}
66
+ const text = `This is the API response: ${JSON.stringify(data)}
67
67
 
68
68
  Additionally, here is a template of what to show to the user:
69
69
 
@@ -124,7 +124,7 @@ export function registerFeatureTools({ server, baseApiUrl, apiKey, appOrigin, us
124
124
  return acc;
125
125
  }, {}),
126
126
  };
127
- const res = await fetch(`${baseApiUrl}/api/v1/features/${featureId}`, {
127
+ const res = await fetchWithRateLimit(`${baseApiUrl}/api/v1/features/${featureId}`, {
128
128
  method: "POST",
129
129
  headers: {
130
130
  Authorization: `Bearer ${apiKey}`,
@@ -136,7 +136,7 @@ export function registerFeatureTools({ server, baseApiUrl, apiKey, appOrigin, us
136
136
  const data = await res.json();
137
137
  const linkToGrowthBook = generateLinkToGrowthBook(appOrigin, "features", featureId);
138
138
  const { docs, language, stub } = getDocsMetadata(fileExtension);
139
- const text = `This is the API response: ${JSON.stringify(data, null, 2)}
139
+ const text = `This is the API response: ${JSON.stringify(data)}
140
140
 
141
141
  Additionally, here is a template of what to show to the user:
142
142
 
@@ -176,7 +176,7 @@ export function registerFeatureTools({ server, baseApiUrl, apiKey, appOrigin, us
176
176
  if (project) {
177
177
  queryParams.append("projectId", project);
178
178
  }
179
- const res = await fetch(`${baseApiUrl}/api/v1/features?${queryParams.toString()}`, {
179
+ const res = await fetchWithRateLimit(`${baseApiUrl}/api/v1/features?${queryParams.toString()}`, {
180
180
  headers: {
181
181
  Authorization: `Bearer ${apiKey}`,
182
182
  "Content-Type": "application/json",
@@ -185,7 +185,7 @@ export function registerFeatureTools({ server, baseApiUrl, apiKey, appOrigin, us
185
185
  await handleResNotOk(res);
186
186
  const data = await res.json();
187
187
  return {
188
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
188
+ content: [{ type: "text", text: JSON.stringify(data) }],
189
189
  };
190
190
  }
191
191
  catch (error) {
@@ -201,7 +201,7 @@ export function registerFeatureTools({ server, baseApiUrl, apiKey, appOrigin, us
201
201
  readOnlyHint: true,
202
202
  }, async ({ id }) => {
203
203
  try {
204
- const res = await fetch(`${baseApiUrl}/api/v1/features/${id}`, {
204
+ const res = await fetchWithRateLimit(`${baseApiUrl}/api/v1/features/${id}`, {
205
205
  headers: {
206
206
  Authorization: `Bearer ${apiKey}`,
207
207
  "Content-Type": "application/json",
@@ -211,7 +211,7 @@ export function registerFeatureTools({ server, baseApiUrl, apiKey, appOrigin, us
211
211
  const data = await res.json();
212
212
  const linkToGrowthBook = generateLinkToGrowthBook(appOrigin, "features", id);
213
213
  const text = `
214
- ${JSON.stringify(data.feature, null, 2)}
214
+ ${JSON.stringify(data)}
215
215
 
216
216
  Share information about the feature flag with the user. In particular, give details about the enabled environments,
217
217
  rules for each environment, and the default value. If the feature flag is archived or doesnt exist, inform the user and
@@ -241,7 +241,7 @@ export function registerFeatureTools({ server, baseApiUrl, apiKey, appOrigin, us
241
241
  limit: limit?.toString(),
242
242
  offset: offset?.toString(),
243
243
  });
244
- const res = await fetch(`${baseApiUrl}/api/v1/features?${queryParams.toString()}`, {
244
+ const res = await fetchWithRateLimit(`${baseApiUrl}/api/v1/features?${queryParams.toString()}`, {
245
245
  headers: {
246
246
  Authorization: `Bearer ${apiKey}`,
247
247
  "Content-Type": "application/json",
@@ -264,7 +264,7 @@ export function registerFeatureTools({ server, baseApiUrl, apiKey, appOrigin, us
264
264
  });
265
265
  });
266
266
  const text = `
267
- ${JSON.stringify(filteredSafeRollouts, null, 2)}
267
+ ${JSON.stringify(filteredSafeRollouts)}
268
268
 
269
269
  Share information about the rolled-back or released safe rollout rules with the user. Safe Rollout rules are stored under
270
270
  environmentSettings, keyed by environment and are within the rules array with a type of "safe-rollout". Ask the user if they
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { generateLinkToGrowthBook, handleResNotOk, paginationSchema, } from "../utils.js";
2
+ import { generateLinkToGrowthBook, handleResNotOk, paginationSchema, fetchWithRateLimit, } from "../utils.js";
3
3
  export function registerMetricsTools({ server, baseApiUrl, apiKey, appOrigin, }) {
4
4
  /**
5
5
  * Tool: get_metrics
@@ -21,7 +21,7 @@ export function registerMetricsTools({ server, baseApiUrl, apiKey, appOrigin, })
21
21
  if (project) {
22
22
  queryParams.append("projectId", project);
23
23
  }
24
- const metricsRes = await fetch(`${baseApiUrl}/api/v1/metrics?${queryParams.toString()}`, {
24
+ const metricsRes = await fetchWithRateLimit(`${baseApiUrl}/api/v1/metrics?${queryParams.toString()}`, {
25
25
  headers: {
26
26
  Authorization: `Bearer ${apiKey}`,
27
27
  "Content-Type": "application/json",
@@ -29,7 +29,7 @@ export function registerMetricsTools({ server, baseApiUrl, apiKey, appOrigin, })
29
29
  });
30
30
  await handleResNotOk(metricsRes);
31
31
  const metricsData = await metricsRes.json();
32
- const factMetricRes = await fetch(`${baseApiUrl}/api/v1/fact-metrics?${queryParams.toString()}`, {
32
+ const factMetricRes = await fetchWithRateLimit(`${baseApiUrl}/api/v1/fact-metrics?${queryParams.toString()}`, {
33
33
  headers: {
34
34
  Authorization: `Bearer ${apiKey}`,
35
35
  "Content-Type": "application/json",
@@ -42,9 +42,7 @@ export function registerMetricsTools({ server, baseApiUrl, apiKey, appOrigin, })
42
42
  factMetrics: factMetricData,
43
43
  };
44
44
  return {
45
- content: [
46
- { type: "text", text: JSON.stringify(metricData, null, 2) },
47
- ],
45
+ content: [{ type: "text", text: JSON.stringify(metricData) }],
48
46
  };
49
47
  }
50
48
  catch (error) {
@@ -62,7 +60,7 @@ export function registerMetricsTools({ server, baseApiUrl, apiKey, appOrigin, })
62
60
  try {
63
61
  let res;
64
62
  if (metricId.startsWith("fact__")) {
65
- res = await fetch(`${baseApiUrl}/api/v1/fact-metrics/${metricId}`, {
63
+ res = await fetchWithRateLimit(`${baseApiUrl}/api/v1/fact-metrics/${metricId}`, {
66
64
  headers: {
67
65
  Authorization: `Bearer ${apiKey}`,
68
66
  "Content-Type": "application/json",
@@ -70,7 +68,7 @@ export function registerMetricsTools({ server, baseApiUrl, apiKey, appOrigin, })
70
68
  });
71
69
  }
72
70
  else {
73
- res = await fetch(`${baseApiUrl}/api/v1/metrics/${metricId}`, {
71
+ res = await fetchWithRateLimit(`${baseApiUrl}/api/v1/metrics/${metricId}`, {
74
72
  headers: {
75
73
  Authorization: `Bearer ${apiKey}`,
76
74
  "Content-Type": "application/json",
@@ -84,7 +82,7 @@ export function registerMetricsTools({ server, baseApiUrl, apiKey, appOrigin, })
84
82
  content: [
85
83
  {
86
84
  type: "text",
87
- text: JSON.stringify(data, null, 2) +
85
+ text: JSON.stringify(data) +
88
86
  `\n**Critical** Show the user the link to the metric in GrowthBook: [View the metric in GrowthBook](${linkToGrowthBook})
89
87
  `,
90
88
  },
@@ -1,4 +1,4 @@
1
- import { handleResNotOk, paginationSchema, } from "../utils.js";
1
+ import { handleResNotOk, paginationSchema, fetchWithRateLimit, } from "../utils.js";
2
2
  /**
3
3
  * Tool: get_projects
4
4
  */
@@ -13,7 +13,7 @@ export function registerProjectTools({ server, baseApiUrl, apiKey, }) {
13
13
  offset: offset.toString(),
14
14
  });
15
15
  try {
16
- const res = await fetch(`${baseApiUrl}/api/v1/projects?${queryParams.toString()}`, {
16
+ const res = await fetchWithRateLimit(`${baseApiUrl}/api/v1/projects?${queryParams.toString()}`, {
17
17
  headers: {
18
18
  Authorization: `Bearer ${apiKey}`,
19
19
  "Content-Type": "application/json",
@@ -22,7 +22,7 @@ export function registerProjectTools({ server, baseApiUrl, apiKey, }) {
22
22
  await handleResNotOk(res);
23
23
  const data = await res.json();
24
24
  return {
25
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
25
+ content: [{ type: "text", text: JSON.stringify(data) }],
26
26
  };
27
27
  }
28
28
  catch (error) {
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { handleResNotOk, paginationSchema, } from "../utils.js";
2
+ import { handleResNotOk, paginationSchema, fetchWithRateLimit, } from "../utils.js";
3
3
  export function registerSdkConnectionTools({ server, baseApiUrl, apiKey, }) {
4
4
  /**
5
5
  * Tool: get_sdk_connections
@@ -21,7 +21,7 @@ export function registerSdkConnectionTools({ server, baseApiUrl, apiKey, }) {
21
21
  if (project) {
22
22
  queryParams.append("projectId", project);
23
23
  }
24
- const res = await fetch(`${baseApiUrl}/api/v1/sdk-connections?${queryParams.toString()}`, {
24
+ const res = await fetchWithRateLimit(`${baseApiUrl}/api/v1/sdk-connections?${queryParams.toString()}`, {
25
25
  headers: {
26
26
  Authorization: `Bearer ${apiKey}`,
27
27
  "Content-Type": "application/json",
@@ -30,7 +30,7 @@ export function registerSdkConnectionTools({ server, baseApiUrl, apiKey, }) {
30
30
  await handleResNotOk(res);
31
31
  const data = await res.json();
32
32
  return {
33
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
33
+ content: [{ type: "text", text: JSON.stringify(data) }],
34
34
  };
35
35
  }
36
36
  catch (error) {
@@ -84,7 +84,7 @@ export function registerSdkConnectionTools({ server, baseApiUrl, apiKey, }) {
84
84
  }, async ({ name, language, environment, projects }) => {
85
85
  if (!environment) {
86
86
  try {
87
- const res = await fetch(`${baseApiUrl}/api/v1/environments`, {
87
+ const res = await fetchWithRateLimit(`${baseApiUrl}/api/v1/environments`, {
88
88
  headers: {
89
89
  Authorization: `Bearer ${apiKey}`,
90
90
  "Content-Type": "application/json",
@@ -92,7 +92,7 @@ export function registerSdkConnectionTools({ server, baseApiUrl, apiKey, }) {
92
92
  });
93
93
  await handleResNotOk(res);
94
94
  const data = await res.json();
95
- const text = `${JSON.stringify(data, null, 2)}
95
+ const text = `${JSON.stringify(data)}
96
96
 
97
97
  Here is the list of environments. Ask the user to select one and use the key in the create_sdk_connection tool.
98
98
  `;
@@ -113,7 +113,7 @@ export function registerSdkConnectionTools({ server, baseApiUrl, apiKey, }) {
113
113
  ...(projects && { projects }),
114
114
  };
115
115
  try {
116
- const res = await fetch(`${baseApiUrl}/api/v1/sdk-connections`, {
116
+ const res = await fetchWithRateLimit(`${baseApiUrl}/api/v1/sdk-connections`, {
117
117
  method: "POST",
118
118
  headers: {
119
119
  Authorization: `Bearer ${apiKey}`,
@@ -124,7 +124,7 @@ export function registerSdkConnectionTools({ server, baseApiUrl, apiKey, }) {
124
124
  await handleResNotOk(res);
125
125
  const data = await res.json();
126
126
  return {
127
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
127
+ content: [{ type: "text", text: JSON.stringify(data) }],
128
128
  };
129
129
  }
130
130
  catch (error) {
@@ -7,35 +7,54 @@ export function registerSearchTools({ server }) {
7
7
  server.tool("search_growthbook_docs", "Search the GrowthBook docs on how to use a feature", {
8
8
  query: z
9
9
  .string()
10
+ .min(1)
10
11
  .describe("The search query to look up in the GrowthBook docs."),
12
+ maxResults: z
13
+ .number()
14
+ .min(1)
15
+ .max(10)
16
+ .default(5)
17
+ .optional()
18
+ .describe("Maximum number of results to return (1-10, default: 5). More results may provide better context but increase response size."),
11
19
  }, {
12
20
  readOnlyHint: true,
13
- }, async ({ query }) => {
14
- const hits = await searchGrowthBookDocs(query);
21
+ }, async ({ query, maxResults = 5 }) => {
22
+ const results = await searchGrowthBookDocs(query, {
23
+ hitsPerPage: maxResults,
24
+ });
25
+ if (results.length === 0) {
26
+ return {
27
+ content: [
28
+ {
29
+ type: "text",
30
+ text: `No results found for "${query}". Try using different keywords or a more specific search term.`,
31
+ },
32
+ ],
33
+ };
34
+ }
15
35
  return {
16
- content: hits.slice(0, 5).map((hit) => {
17
- // Algolia typically returns content in various fields
18
- const content = hit.content ||
19
- hit.text ||
20
- hit._snippetResult?.content?.value ||
21
- hit._highlightResult?.content?.value;
22
- const snippet = hit._snippetResult?.content?.value ||
23
- hit._highlightResult?.content?.value;
24
- const title = hit.title || hit.hierarchy?.lvl0 || hit.hierarchy?.lvl1;
25
- const url = hit.url || hit.anchor;
26
- let text = "";
27
- if (title) {
28
- text += `**${title}**\n`;
36
+ content: results.map((result) => {
37
+ let text = `**${result.title}**\n`;
38
+ // Add hierarchy/breadcrumb if available (helps with context)
39
+ if (result.hierarchy && result.hierarchy.length > 0) {
40
+ text += `📍 ${result.hierarchy.join(" > ")}\n`;
29
41
  }
30
- if (url) {
31
- text += `URL: ${url}\n`;
42
+ // Add URL (important for reference)
43
+ if (result.url && result.url !== "#") {
44
+ text += `🔗 ${result.url}\n`;
32
45
  }
33
- if (snippet || content) {
34
- text += `\n${snippet || content}`;
46
+ // Add snippet/content (the most important part - make it prominent)
47
+ if (result.snippet) {
48
+ text += `\n${result.snippet}`;
49
+ }
50
+ else if (result.content) {
51
+ // Fallback to full content if no snippet, but truncate intelligently
52
+ const truncated = result.content.substring(0, 300);
53
+ text += `\n${truncated}${result.content.length > 300 ? "..." : ""}`;
35
54
  }
36
55
  return {
37
56
  type: "text",
38
- text: text || JSON.stringify(hit),
57
+ text: text.trim(),
39
58
  };
40
59
  }),
41
60
  };
@@ -0,0 +1 @@
1
+ export {};