@growthbook/mcp 1.5.1 → 1.6.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/README.md +18 -0
- package/package.json +1 -1
- package/server/tools/defaults.js +6 -17
- package/server/tools/environments.js +2 -5
- package/server/tools/experiments/experiment-summary.js +3 -9
- package/server/tools/experiments/experiments.js +7 -23
- package/server/tools/features.js +7 -22
- package/server/tools/metrics.js +3 -9
- package/server/tools/sdk-connections.js +3 -9
- package/server/utils.js +60 -16
package/README.md
CHANGED
|
@@ -17,6 +17,24 @@ Use the following env variables to configure the MCP server.
|
|
|
17
17
|
| GB_EMAIL | Required | Your email address used with GrowthBook. Used when creating feature flags and experiments.|
|
|
18
18
|
| GB_API_URL | Optional | Your GrowthBook API URL. Defaults to `https://api.growthbook.io`. |
|
|
19
19
|
| GB_APP_ORIGIN | Optional | Your GrowthBook app URL Defaults to `https://app.growthbook.io`. |
|
|
20
|
+
| GB_HTTP_HEADER_* | Optional | Custom HTTP headers to include in all GrowthBook API requests. Use the pattern `GB_HTTP_HEADER_<NAME>` where `<NAME>` is converted to proper HTTP header format (underscores become hyphens). Examples: `GB_HTTP_HEADER_X_TENANT_ID=abc123` becomes `X-Tenant-ID: abc123`, `GB_HTTP_HEADER_CF_ACCESS_TOKEN=<token>` becomes `Cf-Access-Token: <token>`. Multiple custom headers can be configured. |
|
|
20
21
|
|
|
22
|
+
**Custom Headers Examples**
|
|
23
|
+
|
|
24
|
+
For multi-tenant deployments or proxy configurations, you can add custom headers:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# Multi-tenant identification
|
|
28
|
+
GB_HTTP_HEADER_X_TENANT_ID=tenant-123
|
|
29
|
+
|
|
30
|
+
# Cloudflare Access proxy authentication
|
|
31
|
+
GB_HTTP_HEADER_CF_ACCESS_TOKEN=eyJhbGciOiJSUzI1NiIs...
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Security Best Practices**
|
|
35
|
+
- Always use HTTPS for API communication (default for GrowthBook Cloud)
|
|
36
|
+
- Store API keys and sensitive headers in environment variables, never hardcode them
|
|
37
|
+
- Use the Authorization header (via GB_API_KEY) for authentication
|
|
38
|
+
- Custom headers are useful for multi-tenant scenarios, proxy routing, or additional context
|
|
21
39
|
|
|
22
40
|
Add the MCP server to your AI tool of choice. See the [official docs](https://docs.growthbook.io/integrations/mcp) for complete a complete guide.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@growthbook/mcp",
|
|
3
3
|
"mcpName": "io.github.growthbook/growthbook-mcp",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.6.0",
|
|
5
5
|
"description": "MCP Server for interacting with GrowthBook",
|
|
6
6
|
"access": "public",
|
|
7
7
|
"homepage": "https://github.com/growthbook/growthbook-mcp",
|
package/server/tools/defaults.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { handleResNotOk, fetchWithRateLimit, } from "../utils.js";
|
|
1
|
+
import { handleResNotOk, fetchWithRateLimit, buildHeaders, } 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";
|
|
@@ -10,18 +10,14 @@ const userDefaultsFile = join(experimentDefaultsDir, "user-defaults.json");
|
|
|
10
10
|
export async function createDefaults(apiKey, baseApiUrl) {
|
|
11
11
|
try {
|
|
12
12
|
const experimentsResponse = await fetchWithRateLimit(`${baseApiUrl}/api/v1/experiments`, {
|
|
13
|
-
headers:
|
|
14
|
-
Authorization: `Bearer ${apiKey}`,
|
|
15
|
-
},
|
|
13
|
+
headers: buildHeaders(apiKey, false),
|
|
16
14
|
});
|
|
17
15
|
await handleResNotOk(experimentsResponse);
|
|
18
16
|
const experimentData = await experimentsResponse.json();
|
|
19
17
|
if (experimentData.experiments.length === 0) {
|
|
20
18
|
// No experiments: return assignment query and environments if possible
|
|
21
19
|
const assignmentQueryResponse = await fetchWithRateLimit(`${baseApiUrl}/api/v1/data-sources`, {
|
|
22
|
-
headers:
|
|
23
|
-
Authorization: `Bearer ${apiKey}`,
|
|
24
|
-
},
|
|
20
|
+
headers: buildHeaders(apiKey, false),
|
|
25
21
|
});
|
|
26
22
|
await handleResNotOk(assignmentQueryResponse);
|
|
27
23
|
const dataSourceData = await assignmentQueryResponse.json();
|
|
@@ -30,9 +26,7 @@ export async function createDefaults(apiKey, baseApiUrl) {
|
|
|
30
26
|
}
|
|
31
27
|
const assignmentQuery = dataSourceData.dataSources[0].assignmentQueries[0].id;
|
|
32
28
|
const environmentsResponse = await fetchWithRateLimit(`${baseApiUrl}/api/v1/environments`, {
|
|
33
|
-
headers:
|
|
34
|
-
Authorization: `Bearer ${apiKey}`,
|
|
35
|
-
},
|
|
29
|
+
headers: buildHeaders(apiKey, false),
|
|
36
30
|
});
|
|
37
31
|
await handleResNotOk(environmentsResponse);
|
|
38
32
|
const environmentsData = await environmentsResponse.json();
|
|
@@ -55,10 +49,7 @@ export async function createDefaults(apiKey, baseApiUrl) {
|
|
|
55
49
|
if (experimentData.hasMore) {
|
|
56
50
|
const mostRecentExperiments = await fetchWithRateLimit(`${baseApiUrl}/api/v1/experiments?offset=${experimentData.total -
|
|
57
51
|
Math.min(50, experimentData.count + experimentData.offset)}&limit=${Math.min(50, experimentData.count + experimentData.offset)}`, {
|
|
58
|
-
headers:
|
|
59
|
-
Authorization: `Bearer ${apiKey}`,
|
|
60
|
-
"Content-Type": "application/json",
|
|
61
|
-
},
|
|
52
|
+
headers: buildHeaders(apiKey),
|
|
62
53
|
});
|
|
63
54
|
await handleResNotOk(mostRecentExperiments);
|
|
64
55
|
const mostRecentExperimentData = await mostRecentExperiments.json();
|
|
@@ -113,9 +104,7 @@ export async function createDefaults(apiKey, baseApiUrl) {
|
|
|
113
104
|
}
|
|
114
105
|
// Fetch environments
|
|
115
106
|
const environmentsResponse = await fetchWithRateLimit(`${baseApiUrl}/api/v1/environments`, {
|
|
116
|
-
headers:
|
|
117
|
-
Authorization: `Bearer ${apiKey}`,
|
|
118
|
-
},
|
|
107
|
+
headers: buildHeaders(apiKey, false),
|
|
119
108
|
});
|
|
120
109
|
await handleResNotOk(environmentsResponse);
|
|
121
110
|
const environmentsData = await environmentsResponse.json();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { handleResNotOk, fetchWithRateLimit, } from "../utils.js";
|
|
1
|
+
import { handleResNotOk, fetchWithRateLimit, buildHeaders, } from "../utils.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
/**
|
|
4
4
|
* Tool: get_environments
|
|
@@ -14,10 +14,7 @@ export function registerEnvironmentTools({ server, baseApiUrl, apiKey, }) {
|
|
|
14
14
|
}, async () => {
|
|
15
15
|
try {
|
|
16
16
|
const res = await fetchWithRateLimit(`${baseApiUrl}/api/v1/environments`, {
|
|
17
|
-
headers:
|
|
18
|
-
Authorization: `Bearer ${apiKey}`,
|
|
19
|
-
"Content-Type": "application/json",
|
|
20
|
-
},
|
|
17
|
+
headers: buildHeaders(apiKey),
|
|
21
18
|
});
|
|
22
19
|
await handleResNotOk(res);
|
|
23
20
|
const data = await res.json();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { fetchWithRateLimit, handleResNotOk } from "../../utils.js";
|
|
1
|
+
import { fetchWithRateLimit, handleResNotOk, buildHeaders } from "../../utils.js";
|
|
2
2
|
import { computeVerdict, formatLift, getYearMonth, median, round, } from "./summary-logic.js";
|
|
3
3
|
// Metric Lookup with caching
|
|
4
4
|
const metricCache = new Map();
|
|
@@ -48,10 +48,7 @@ async function getMetricLookup(baseApiUrl, apiKey, metricIds) {
|
|
|
48
48
|
const regularResults = await processBatch(regularMetricIds, MAX_CONCURRENT_FETCHES, async (metricId) => {
|
|
49
49
|
try {
|
|
50
50
|
const res = await fetchWithRateLimit(`${baseApiUrl}/api/v1/metrics/${metricId}`, {
|
|
51
|
-
headers:
|
|
52
|
-
Authorization: `Bearer ${apiKey}`,
|
|
53
|
-
"Content-Type": "application/json",
|
|
54
|
-
},
|
|
51
|
+
headers: buildHeaders(apiKey),
|
|
55
52
|
});
|
|
56
53
|
await handleResNotOk(res);
|
|
57
54
|
const data = await res.json();
|
|
@@ -74,10 +71,7 @@ async function getMetricLookup(baseApiUrl, apiKey, metricIds) {
|
|
|
74
71
|
const factResults = await processBatch(factMetricIds, MAX_CONCURRENT_FETCHES, async (metricId) => {
|
|
75
72
|
try {
|
|
76
73
|
const res = await fetchWithRateLimit(`${baseApiUrl}/api/v1/fact-metrics/${metricId}`, {
|
|
77
|
-
headers:
|
|
78
|
-
Authorization: `Bearer ${apiKey}`,
|
|
79
|
-
"Content-Type": "application/json",
|
|
80
|
-
},
|
|
74
|
+
headers: buildHeaders(apiKey),
|
|
81
75
|
});
|
|
82
76
|
await handleResNotOk(res);
|
|
83
77
|
const data = await res.json();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { generateLinkToGrowthBook, getDocsMetadata, handleResNotOk, SUPPORTED_FILE_EXTENSIONS, paginationSchema, fetchWithRateLimit, fetchWithPagination, featureFlagSchema, fetchFeatureFlag, mergeRuleIntoFeatureFlag, } from "../../utils.js";
|
|
2
|
+
import { generateLinkToGrowthBook, getDocsMetadata, handleResNotOk, SUPPORTED_FILE_EXTENSIONS, paginationSchema, fetchWithRateLimit, fetchWithPagination, featureFlagSchema, fetchFeatureFlag, mergeRuleIntoFeatureFlag, buildHeaders, } from "../../utils.js";
|
|
3
3
|
import { getDefaults } from "../defaults.js";
|
|
4
4
|
import { handleSummaryMode } from "./experiment-summary.js";
|
|
5
5
|
export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin, user, }) {
|
|
@@ -31,10 +31,7 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
|
|
|
31
31
|
if (experimentId) {
|
|
32
32
|
try {
|
|
33
33
|
const res = await fetchWithRateLimit(`${baseApiUrl}/api/v1/experiments/${experimentId}`, {
|
|
34
|
-
headers:
|
|
35
|
-
Authorization: `Bearer ${apiKey}`,
|
|
36
|
-
"Content-Type": "application/json",
|
|
37
|
-
},
|
|
34
|
+
headers: buildHeaders(apiKey),
|
|
38
35
|
});
|
|
39
36
|
await handleResNotOk(res);
|
|
40
37
|
const data = await res.json();
|
|
@@ -45,9 +42,7 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
|
|
|
45
42
|
}
|
|
46
43
|
try {
|
|
47
44
|
const resultsRes = await fetchWithRateLimit(`${baseApiUrl}/api/v1/experiments/${experimentId}/results`, {
|
|
48
|
-
headers:
|
|
49
|
-
Authorization: `Bearer ${apiKey}`,
|
|
50
|
-
},
|
|
45
|
+
headers: buildHeaders(apiKey, false),
|
|
51
46
|
});
|
|
52
47
|
await handleResNotOk(resultsRes);
|
|
53
48
|
const resultsData = await resultsRes.json();
|
|
@@ -104,9 +99,7 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
|
|
|
104
99
|
}
|
|
105
100
|
try {
|
|
106
101
|
const resultsRes = await fetchWithRateLimit(`${baseApiUrl}/api/v1/experiments/${experiment.id}/results`, {
|
|
107
|
-
headers:
|
|
108
|
-
Authorization: `Bearer ${apiKey}`,
|
|
109
|
-
},
|
|
102
|
+
headers: buildHeaders(apiKey, false),
|
|
110
103
|
});
|
|
111
104
|
await handleResNotOk(resultsRes);
|
|
112
105
|
const resultsData = await resultsRes.json();
|
|
@@ -160,10 +153,7 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
|
|
|
160
153
|
const queryParams = new URLSearchParams();
|
|
161
154
|
queryParams.append("limit", "100");
|
|
162
155
|
const res = await fetchWithRateLimit(`${baseApiUrl}/api/v1/attributes?${queryParams.toString()}`, {
|
|
163
|
-
headers:
|
|
164
|
-
Authorization: `Bearer ${apiKey}`,
|
|
165
|
-
"Content-Type": "application/json",
|
|
166
|
-
},
|
|
156
|
+
headers: buildHeaders(apiKey),
|
|
167
157
|
});
|
|
168
158
|
await handleResNotOk(res);
|
|
169
159
|
const data = await res.json();
|
|
@@ -258,10 +248,7 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
|
|
|
258
248
|
try {
|
|
259
249
|
const experimentRes = await fetchWithRateLimit(`${baseApiUrl}/api/v1/experiments`, {
|
|
260
250
|
method: "POST",
|
|
261
|
-
headers:
|
|
262
|
-
Authorization: `Bearer ${apiKey}`,
|
|
263
|
-
"Content-Type": "application/json",
|
|
264
|
-
},
|
|
251
|
+
headers: buildHeaders(apiKey),
|
|
265
252
|
body: JSON.stringify(experimentPayload),
|
|
266
253
|
});
|
|
267
254
|
await handleResNotOk(experimentRes);
|
|
@@ -283,10 +270,7 @@ export function registerExperimentTools({ server, baseApiUrl, apiKey, appOrigin,
|
|
|
283
270
|
const flagPayload = mergeRuleIntoFeatureFlag(existingFeature, newRule, experimentDefaults.environments);
|
|
284
271
|
const flagRes = await fetchWithRateLimit(`${baseApiUrl}/api/v1/features/${featureId}`, {
|
|
285
272
|
method: "POST",
|
|
286
|
-
headers:
|
|
287
|
-
Authorization: `Bearer ${apiKey}`,
|
|
288
|
-
"Content-Type": "application/json",
|
|
289
|
-
},
|
|
273
|
+
headers: buildHeaders(apiKey),
|
|
290
274
|
body: JSON.stringify(flagPayload),
|
|
291
275
|
});
|
|
292
276
|
await handleResNotOk(flagRes);
|
package/server/tools/features.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { getDocsMetadata, handleResNotOk, generateLinkToGrowthBook, paginationSchema, featureFlagSchema, fetchWithRateLimit, fetchWithPagination, fetchFeatureFlag, mergeRuleIntoFeatureFlag, } from "../utils.js";
|
|
2
|
+
import { getDocsMetadata, handleResNotOk, generateLinkToGrowthBook, paginationSchema, featureFlagSchema, fetchWithRateLimit, fetchWithPagination, fetchFeatureFlag, mergeRuleIntoFeatureFlag, buildHeaders, } 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, }) {
|
|
@@ -23,10 +23,7 @@ export function registerFeatureTools({ server, baseApiUrl, apiKey, appOrigin, us
|
|
|
23
23
|
}
|
|
24
24
|
else {
|
|
25
25
|
const envRes = await fetchWithRateLimit(`${baseApiUrl}/api/v1/features/environments`, {
|
|
26
|
-
headers:
|
|
27
|
-
Authorization: `Bearer ${apiKey}`,
|
|
28
|
-
"Content-Type": "application/json",
|
|
29
|
-
},
|
|
26
|
+
headers: buildHeaders(apiKey),
|
|
30
27
|
});
|
|
31
28
|
await handleResNotOk(envRes);
|
|
32
29
|
const envData = await envRes.json();
|
|
@@ -51,10 +48,7 @@ export function registerFeatureTools({ server, baseApiUrl, apiKey, appOrigin, us
|
|
|
51
48
|
try {
|
|
52
49
|
const res = await fetchWithRateLimit(`${baseApiUrl}/api/v1/features`, {
|
|
53
50
|
method: "POST",
|
|
54
|
-
headers:
|
|
55
|
-
Authorization: `Bearer ${apiKey}`,
|
|
56
|
-
"Content-Type": "application/json",
|
|
57
|
-
},
|
|
51
|
+
headers: buildHeaders(apiKey),
|
|
58
52
|
body: JSON.stringify(payload),
|
|
59
53
|
});
|
|
60
54
|
await handleResNotOk(res);
|
|
@@ -123,10 +117,7 @@ export function registerFeatureTools({ server, baseApiUrl, apiKey, appOrigin, us
|
|
|
123
117
|
const payload = mergeRuleIntoFeatureFlag(existingFeature, newRule, defaultEnvironments);
|
|
124
118
|
const res = await fetchWithRateLimit(`${baseApiUrl}/api/v1/features/${featureId}`, {
|
|
125
119
|
method: "POST",
|
|
126
|
-
headers:
|
|
127
|
-
Authorization: `Bearer ${apiKey}`,
|
|
128
|
-
"Content-Type": "application/json",
|
|
129
|
-
},
|
|
120
|
+
headers: buildHeaders(apiKey),
|
|
130
121
|
body: JSON.stringify(payload),
|
|
131
122
|
});
|
|
132
123
|
await handleResNotOk(res);
|
|
@@ -134,7 +125,7 @@ export function registerFeatureTools({ server, baseApiUrl, apiKey, appOrigin, us
|
|
|
134
125
|
const linkToGrowthBook = generateLinkToGrowthBook(appOrigin, "features", featureId);
|
|
135
126
|
const { docs, language, stub } = getDocsMetadata(fileExtension);
|
|
136
127
|
const text = `This is the API response: ${JSON.stringify(data)}
|
|
137
|
-
|
|
128
|
+
|
|
138
129
|
Additionally, here is a template of what to show to the user:
|
|
139
130
|
|
|
140
131
|
**✅ Your feature flag \`my-flag-name\` is ready!.**
|
|
@@ -175,10 +166,7 @@ export function registerFeatureTools({ server, baseApiUrl, apiKey, appOrigin, us
|
|
|
175
166
|
if (featureFlagId) {
|
|
176
167
|
try {
|
|
177
168
|
const res = await fetchWithRateLimit(`${baseApiUrl}/api/v1/features/${featureFlagId}`, {
|
|
178
|
-
headers:
|
|
179
|
-
Authorization: `Bearer ${apiKey}`,
|
|
180
|
-
"Content-Type": "application/json",
|
|
181
|
-
},
|
|
169
|
+
headers: buildHeaders(apiKey),
|
|
182
170
|
});
|
|
183
171
|
await handleResNotOk(res);
|
|
184
172
|
const data = await res.json();
|
|
@@ -233,10 +221,7 @@ ask if they want to remove references to the feature flag from the codebase.
|
|
|
233
221
|
offset: offset?.toString(),
|
|
234
222
|
});
|
|
235
223
|
const res = await fetchWithRateLimit(`${baseApiUrl}/api/v1/features?${queryParams.toString()}`, {
|
|
236
|
-
headers:
|
|
237
|
-
Authorization: `Bearer ${apiKey}`,
|
|
238
|
-
"Content-Type": "application/json",
|
|
239
|
-
},
|
|
224
|
+
headers: buildHeaders(apiKey),
|
|
240
225
|
});
|
|
241
226
|
await handleResNotOk(res);
|
|
242
227
|
const data = await res.json();
|
package/server/tools/metrics.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { generateLinkToGrowthBook, handleResNotOk, paginationSchema, fetchWithRateLimit, fetchWithPagination, } from "../utils.js";
|
|
2
|
+
import { generateLinkToGrowthBook, handleResNotOk, paginationSchema, fetchWithRateLimit, fetchWithPagination, buildHeaders, } from "../utils.js";
|
|
3
3
|
export function registerMetricsTools({ server, baseApiUrl, apiKey, appOrigin, }) {
|
|
4
4
|
/**
|
|
5
5
|
* Tool: get_metrics
|
|
@@ -27,18 +27,12 @@ export function registerMetricsTools({ server, baseApiUrl, apiKey, appOrigin, })
|
|
|
27
27
|
let res;
|
|
28
28
|
if (metricId.startsWith("fact__")) {
|
|
29
29
|
res = await fetchWithRateLimit(`${baseApiUrl}/api/v1/fact-metrics/${metricId}`, {
|
|
30
|
-
headers:
|
|
31
|
-
Authorization: `Bearer ${apiKey}`,
|
|
32
|
-
"Content-Type": "application/json",
|
|
33
|
-
},
|
|
30
|
+
headers: buildHeaders(apiKey),
|
|
34
31
|
});
|
|
35
32
|
}
|
|
36
33
|
else {
|
|
37
34
|
res = await fetchWithRateLimit(`${baseApiUrl}/api/v1/metrics/${metricId}`, {
|
|
38
|
-
headers:
|
|
39
|
-
Authorization: `Bearer ${apiKey}`,
|
|
40
|
-
"Content-Type": "application/json",
|
|
41
|
-
},
|
|
35
|
+
headers: buildHeaders(apiKey),
|
|
42
36
|
});
|
|
43
37
|
}
|
|
44
38
|
await handleResNotOk(res);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { handleResNotOk, paginationSchema, fetchWithRateLimit, fetchWithPagination, } from "../utils.js";
|
|
2
|
+
import { handleResNotOk, paginationSchema, fetchWithRateLimit, fetchWithPagination, buildHeaders, } from "../utils.js";
|
|
3
3
|
export function registerSdkConnectionTools({ server, baseApiUrl, apiKey, }) {
|
|
4
4
|
/**
|
|
5
5
|
* Tool: get_sdk_connections
|
|
@@ -85,10 +85,7 @@ export function registerSdkConnectionTools({ server, baseApiUrl, apiKey, }) {
|
|
|
85
85
|
if (!environment) {
|
|
86
86
|
try {
|
|
87
87
|
const res = await fetchWithRateLimit(`${baseApiUrl}/api/v1/environments`, {
|
|
88
|
-
headers:
|
|
89
|
-
Authorization: `Bearer ${apiKey}`,
|
|
90
|
-
"Content-Type": "application/json",
|
|
91
|
-
},
|
|
88
|
+
headers: buildHeaders(apiKey),
|
|
92
89
|
});
|
|
93
90
|
await handleResNotOk(res);
|
|
94
91
|
const data = await res.json();
|
|
@@ -115,10 +112,7 @@ export function registerSdkConnectionTools({ server, baseApiUrl, apiKey, }) {
|
|
|
115
112
|
try {
|
|
116
113
|
const res = await fetchWithRateLimit(`${baseApiUrl}/api/v1/sdk-connections`, {
|
|
117
114
|
method: "POST",
|
|
118
|
-
headers:
|
|
119
|
-
Authorization: `Bearer ${apiKey}`,
|
|
120
|
-
"Content-Type": "application/json",
|
|
121
|
-
},
|
|
115
|
+
headers: buildHeaders(apiKey),
|
|
122
116
|
body: JSON.stringify(payload),
|
|
123
117
|
});
|
|
124
118
|
await handleResNotOk(res);
|
package/server/utils.js
CHANGED
|
@@ -56,6 +56,62 @@ export function getAppOrigin() {
|
|
|
56
56
|
userAppOrigin = userAppOrigin?.trim().replace(/\/+$/, "");
|
|
57
57
|
return `${userAppOrigin || defaultAppOrigin}`;
|
|
58
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Parses custom HTTP headers from environment variables with the prefix GB_HTTP_HEADER_*
|
|
61
|
+
* Converts environment variable names to proper HTTP header format:
|
|
62
|
+
* GB_HTTP_HEADER_X_TENANT_ID -> X-Tenant-ID
|
|
63
|
+
* GB_HTTP_HEADER_CF_ACCESS_TOKEN -> Cf-Access-Token
|
|
64
|
+
*
|
|
65
|
+
* Example usage:
|
|
66
|
+
* GB_HTTP_HEADER_X_TENANT_ID=abc123 -> { "X-Tenant-ID": "abc123" }
|
|
67
|
+
* GB_HTTP_HEADER_CF_ACCESS_TOKEN=<token> -> { "Cf-Access-Token": "<token>" }
|
|
68
|
+
*/
|
|
69
|
+
export function getCustomHeaders() {
|
|
70
|
+
const customHeaders = {};
|
|
71
|
+
const headerPrefix = "GB_HTTP_HEADER_";
|
|
72
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
73
|
+
if (key.startsWith(headerPrefix) && value) {
|
|
74
|
+
// Extract the header name part after the prefix
|
|
75
|
+
const headerNamePart = key.slice(headerPrefix.length);
|
|
76
|
+
// Convert underscore-separated name to proper HTTP header format
|
|
77
|
+
// Example: X_TENANT_ID -> X-Tenant-ID, CF_ACCESS_TOKEN -> Cf-Access-Token
|
|
78
|
+
const headerName = headerNamePart
|
|
79
|
+
.split("_")
|
|
80
|
+
.map((part, index) => {
|
|
81
|
+
// Special handling for common prefixes like X, API, etc.
|
|
82
|
+
if (part.length === 1 || part === "API" || part === "ID") {
|
|
83
|
+
return part;
|
|
84
|
+
}
|
|
85
|
+
// Capitalize first letter, lowercase the rest
|
|
86
|
+
return part.charAt(0).toUpperCase() + part.slice(1).toLowerCase();
|
|
87
|
+
})
|
|
88
|
+
.join("-");
|
|
89
|
+
customHeaders[headerName] = value;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return customHeaders;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Builds HTTP headers for GrowthBook API requests, merging required headers
|
|
96
|
+
* with any custom headers configured via GB_HTTP_HEADER_* environment variables.
|
|
97
|
+
*
|
|
98
|
+
* Custom headers are applied first, then required headers (Authorization, Content-Type)
|
|
99
|
+
* are added. This ensures required headers always take precedence.
|
|
100
|
+
*
|
|
101
|
+
* @param apiKey - The GrowthBook API key for authorization
|
|
102
|
+
* @param includeContentType - Whether to include Content-Type header (default: true)
|
|
103
|
+
* @returns Headers object ready for fetch requests
|
|
104
|
+
*/
|
|
105
|
+
export function buildHeaders(apiKey, includeContentType = true) {
|
|
106
|
+
const headers = {
|
|
107
|
+
...getCustomHeaders(),
|
|
108
|
+
Authorization: `Bearer ${apiKey}`,
|
|
109
|
+
};
|
|
110
|
+
if (includeContentType) {
|
|
111
|
+
headers["Content-Type"] = "application/json";
|
|
112
|
+
}
|
|
113
|
+
return headers;
|
|
114
|
+
}
|
|
59
115
|
export function getDocsMetadata(extension) {
|
|
60
116
|
switch (extension) {
|
|
61
117
|
case ".tsx":
|
|
@@ -372,10 +428,7 @@ export async function fetchWithRateLimit(url, options, retries = 3) {
|
|
|
372
428
|
*/
|
|
373
429
|
export async function fetchFeatureFlag(baseApiUrl, apiKey, featureId) {
|
|
374
430
|
const res = await fetchWithRateLimit(`${baseApiUrl}/api/v1/features/${featureId}`, {
|
|
375
|
-
headers:
|
|
376
|
-
Authorization: `Bearer ${apiKey}`,
|
|
377
|
-
"Content-Type": "application/json",
|
|
378
|
-
},
|
|
431
|
+
headers: buildHeaders(apiKey),
|
|
379
432
|
});
|
|
380
433
|
await handleResNotOk(res);
|
|
381
434
|
const data = await res.json();
|
|
@@ -432,20 +485,14 @@ export async function fetchWithPagination(baseApiUrl, apiKey, endpoint, limit, o
|
|
|
432
485
|
});
|
|
433
486
|
}
|
|
434
487
|
const res = await fetchWithRateLimit(`${baseApiUrl}${endpoint}?${queryParams.toString()}`, {
|
|
435
|
-
headers:
|
|
436
|
-
Authorization: `Bearer ${apiKey}`,
|
|
437
|
-
"Content-Type": "application/json",
|
|
438
|
-
},
|
|
488
|
+
headers: buildHeaders(apiKey),
|
|
439
489
|
});
|
|
440
490
|
await handleResNotOk(res);
|
|
441
491
|
return await res.json();
|
|
442
492
|
}
|
|
443
493
|
// Most recent behavior: fetch total count first, then calculate offset
|
|
444
494
|
const countRes = await fetchWithRateLimit(`${baseApiUrl}${endpoint}?limit=1`, {
|
|
445
|
-
headers:
|
|
446
|
-
Authorization: `Bearer ${apiKey}`,
|
|
447
|
-
"Content-Type": "application/json",
|
|
448
|
-
},
|
|
495
|
+
headers: buildHeaders(apiKey),
|
|
449
496
|
});
|
|
450
497
|
await handleResNotOk(countRes);
|
|
451
498
|
const countData = await countRes.json();
|
|
@@ -464,10 +511,7 @@ export async function fetchWithPagination(baseApiUrl, apiKey, endpoint, limit, o
|
|
|
464
511
|
});
|
|
465
512
|
}
|
|
466
513
|
const mostRecentRes = await fetchWithRateLimit(`${baseApiUrl}${endpoint}?${mostRecentQueryParams.toString()}`, {
|
|
467
|
-
headers:
|
|
468
|
-
Authorization: `Bearer ${apiKey}`,
|
|
469
|
-
"Content-Type": "application/json",
|
|
470
|
-
},
|
|
514
|
+
headers: buildHeaders(apiKey),
|
|
471
515
|
});
|
|
472
516
|
await handleResNotOk(mostRecentRes);
|
|
473
517
|
return await mostRecentRes.json();
|