@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.
- package/package.json +6 -2
- package/server/index.js +11 -4
- package/server/prompts/experiment-prompts.js +30 -0
- package/server/tools/defaults.js +12 -12
- package/server/tools/environments.js +3 -3
- package/server/tools/experiments/experiment-summary.js +494 -0
- package/server/tools/{experiments.js → experiments/experiments.js} +95 -29
- package/server/tools/features.js +12 -12
- package/server/tools/metrics.js +7 -9
- package/server/tools/projects.js +3 -3
- package/server/tools/sdk-connections.js +7 -7
- package/server/tools/search.js +39 -20
- package/server/types/types.js +1 -0
- package/server/utils.js +149 -6
- package/server/prompts/experiment-analysis.js +0 -13
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { generateLinkToGrowthBook, getDocsMetadata, handleResNotOk, SUPPORTED_FILE_EXTENSIONS, paginationSchema, } from "
|
|
3
|
-
import { getDefaults } from "
|
|
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(["
|
|
15
|
-
.default("
|
|
16
|
-
.describe("The mode to use to fetch experiments.
|
|
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
|
|
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 === "
|
|
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
|
|
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
|
|
100
|
+
content: [{ type: "text", text: JSON.stringify(data) }],
|
|
59
101
|
};
|
|
60
102
|
}
|
|
61
103
|
// Most recent behavior
|
|
62
|
-
const countRes = await
|
|
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
|
|
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 === "
|
|
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
|
|
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(["
|
|
123
|
-
.default("
|
|
124
|
-
.describe("The mode to use to fetch the experiment.
|
|
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
|
|
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 === "
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
367
|
+
const flagRes = await fetchWithRateLimit(`${baseApiUrl}/api/v1/features`, {
|
|
302
368
|
method: "POST",
|
|
303
369
|
headers: {
|
|
304
370
|
Authorization: `Bearer ${apiKey}`,
|
package/server/tools/features.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
package/server/tools/metrics.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
},
|
package/server/tools/projects.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
127
|
+
content: [{ type: "text", text: JSON.stringify(data) }],
|
|
128
128
|
};
|
|
129
129
|
}
|
|
130
130
|
catch (error) {
|
package/server/tools/search.js
CHANGED
|
@@ -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
|
|
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:
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
31
|
-
|
|
42
|
+
// Add URL (important for reference)
|
|
43
|
+
if (result.url && result.url !== "#") {
|
|
44
|
+
text += `🔗 ${result.url}\n`;
|
|
32
45
|
}
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
57
|
+
text: text.trim(),
|
|
39
58
|
};
|
|
40
59
|
}),
|
|
41
60
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|