Skip to content

Commit 6b2f378

Browse files
chore(internal): support x-stainless-mcp-client-envs header in MCP servers
1 parent 419e72f commit 6b2f378

File tree

4 files changed

+47
-20
lines changed

4 files changed

+47
-20
lines changed

packages/mcp-server/src/code-tool.ts

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -144,22 +144,25 @@ const remoteStainlessHandler = async ({
144144

145145
const codeModeEndpoint = readEnv('CODE_MODE_ENDPOINT_URL') ?? 'https://api.stainless.com/api/ai/code-tool';
146146

147+
const localClientEnvs = {
148+
IMAGEKIT_PRIVATE_KEY: requireValue(
149+
readEnv('IMAGEKIT_PRIVATE_KEY') ?? client.privateKey,
150+
'set IMAGEKIT_PRIVATE_KEY environment variable or provide privateKey client option',
151+
),
152+
OPTIONAL_IMAGEKIT_IGNORES_THIS: readEnv('OPTIONAL_IMAGEKIT_IGNORES_THIS') ?? client.password ?? undefined,
153+
IMAGEKIT_WEBHOOK_SECRET: readEnv('IMAGEKIT_WEBHOOK_SECRET') ?? client.webhookSecret ?? undefined,
154+
IMAGE_KIT_BASE_URL: readEnv('IMAGE_KIT_BASE_URL') ?? client.baseURL ?? undefined,
155+
};
156+
// Merge any upstream client envs from the request header, with upstream values taking precedence.
157+
const mergedClientEnvs = { ...localClientEnvs, ...reqContext.upstreamClientEnvs };
158+
147159
// Setting a Stainless API key authenticates requests to the code tool endpoint.
148160
const res = await fetch(codeModeEndpoint, {
149161
method: 'POST',
150162
headers: {
151163
...(reqContext.stainlessApiKey && { Authorization: reqContext.stainlessApiKey }),
152164
'Content-Type': 'application/json',
153-
'x-stainless-mcp-client-envs': JSON.stringify({
154-
IMAGEKIT_PRIVATE_KEY: requireValue(
155-
readEnv('IMAGEKIT_PRIVATE_KEY') ?? client.privateKey,
156-
'set IMAGEKIT_PRIVATE_KEY environment variable or provide privateKey client option',
157-
),
158-
OPTIONAL_IMAGEKIT_IGNORES_THIS:
159-
readEnv('OPTIONAL_IMAGEKIT_IGNORES_THIS') ?? client.password ?? undefined,
160-
IMAGEKIT_WEBHOOK_SECRET: readEnv('IMAGEKIT_WEBHOOK_SECRET') ?? client.webhookSecret ?? undefined,
161-
IMAGE_KIT_BASE_URL: readEnv('IMAGE_KIT_BASE_URL') ?? client.baseURL ?? undefined,
162-
}),
165+
'x-stainless-mcp-client-envs': JSON.stringify(mergedClientEnvs),
163166
},
164167
body: JSON.stringify({
165168
project_name: 'imagekit',
@@ -270,6 +273,9 @@ const localDenoHandler = async ({
270273
printOutput: true,
271274
spawnOptions: {
272275
cwd: path.dirname(workerPath),
276+
// Merge any upstream client envs into the Deno subprocess environment,
277+
// with the upstream env vars taking precedence.
278+
env: { ...process.env, ...reqContext.upstreamClientEnvs },
273279
},
274280
});
275281

@@ -279,15 +285,19 @@ const localDenoHandler = async ({
279285
reject(new Error(`Worker exited with code ${exitCode}`));
280286
});
281287

282-
const opts: ClientOptions = {
283-
baseURL: client.baseURL,
284-
privateKey: client.privateKey,
285-
password: client.password,
286-
webhookSecret: client.webhookSecret,
287-
defaultHeaders: {
288-
'X-Stainless-MCP': 'true',
289-
},
290-
};
288+
// Strip null/undefined values so that the worker SDK client can fall back to
289+
// reading from environment variables (including any upstreamClientEnvs).
290+
const opts: ClientOptions = Object.fromEntries(
291+
Object.entries({
292+
baseURL: client.baseURL,
293+
privateKey: client.privateKey,
294+
password: client.password,
295+
webhookSecret: client.webhookSecret,
296+
defaultHeaders: {
297+
'X-Stainless-MCP': 'true',
298+
},
299+
}).filter(([_, v]) => v != null),
300+
) as ClientOptions;
291301

292302
const req = worker.request(
293303
'http://localhost',

packages/mcp-server/src/http.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,19 @@ const newServer = async ({
2727

2828
const authOptions = parseClientAuthHeaders(req, false);
2929

30+
let upstreamClientEnvs: Record<string, string> | undefined;
31+
const clientEnvsHeader = req.headers['x-stainless-mcp-client-envs'];
32+
if (typeof clientEnvsHeader === 'string') {
33+
try {
34+
const parsed = JSON.parse(clientEnvsHeader);
35+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
36+
upstreamClientEnvs = parsed;
37+
}
38+
} catch {
39+
// Ignore malformed header
40+
}
41+
}
42+
3043
await initMcpServer({
3144
server: server,
3245
mcpOptions: mcpOptions,
@@ -35,6 +48,7 @@ const newServer = async ({
3548
...authOptions,
3649
},
3750
stainlessApiKey: stainlessApiKey,
51+
upstreamClientEnvs,
3852
});
3953

4054
return server;
@@ -72,7 +86,7 @@ const del = async (req: express.Request, res: express.Response) => {
7286
};
7387

7488
const redactHeaders = (headers: Record<string, any>) => {
75-
const hiddenHeaders = /auth|cookie|key|token/i;
89+
const hiddenHeaders = /auth|cookie|key|token|x-stainless-mcp-client-envs/i;
7690
const filtered = { ...headers };
7791
Object.keys(filtered).forEach((key) => {
7892
if (hiddenHeaders.test(key)) {

packages/mcp-server/src/server.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export async function initMcpServer(params: {
3737
clientOptions?: ClientOptions;
3838
mcpOptions?: McpOptions;
3939
stainlessApiKey?: string | undefined;
40+
upstreamClientEnvs?: Record<string, string> | undefined;
4041
}) {
4142
const server = params.server instanceof McpServer ? params.server.server : params.server;
4243

@@ -118,6 +119,7 @@ export async function initMcpServer(params: {
118119
reqContext: {
119120
client,
120121
stainlessApiKey: params.stainlessApiKey ?? params.mcpOptions?.stainlessApiKey,
122+
upstreamClientEnvs: params.upstreamClientEnvs,
121123
},
122124
args,
123125
});

packages/mcp-server/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export type ToolCallResult = {
4545
export type McpRequestContext = {
4646
client: ImageKit;
4747
stainlessApiKey?: string | undefined;
48+
upstreamClientEnvs?: Record<string, string> | undefined;
4849
};
4950

5051
export type HandlerFunction = ({

0 commit comments

Comments
 (0)