Skip to content

Commit 1c03006

Browse files
author
stlc-bot
committed
ci: add workflow_dispatch trigger to generate
Stainless-Generated-From: 1c60f594a092b548667fb71265c5650dc4bc8c65
1 parent 7981955 commit 1c03006

4 files changed

Lines changed: 111 additions & 4 deletions

File tree

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

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { WorkerOutput } from './code-tool-types';
66
import { getLogger } from './logger';
77
import { SdkMethod } from './methods';
88
import { McpCodeExecutionMode } from './options';
9+
import { remoteSandboxHandler } from './remote-sandbox-handler';
910
import { ClientOptions } from '@coingecko/coingecko-typescript';
1011

1112
const prompt = `Runs JavaScript code to interact with the Coingecko API.
@@ -39,14 +40,21 @@ Always type dynamic key-value stores explicitly as Record<string, YourValueType>
3940
* matching, so it is not secure against obfuscation. For stronger security, block in the downstream API
4041
* with limited API keys.
4142
* @param codeExecutionMode - Whether to execute code in a local Deno environment or in a remote
42-
* sandbox environment hosted by Stainless.
43+
* sandbox (see remote-sandbox-handler.ts).
44+
* @param sandboxEndpoint - URL of the remote sandbox's code-tool endpoint ('remote-sandbox' mode only).
45+
* @param sandboxFetcher - Optional fetch used to reach the sandbox, e.g. a Cloudflare service-binding
46+
* fetch. Defaults to global fetch.
4347
*/
4448
export function codeTool({
4549
blockedMethods,
4650
codeExecutionMode,
51+
sandboxEndpoint,
52+
sandboxFetcher,
4753
}: {
4854
blockedMethods: SdkMethod[] | undefined;
4955
codeExecutionMode: McpCodeExecutionMode;
56+
sandboxEndpoint?: string | undefined;
57+
sandboxFetcher?: typeof fetch | undefined;
5058
}): McpTool {
5159
const metadata: Metadata = { resource: 'all', operation: 'write', tags: [] };
5260
const tool: Tool = {
@@ -100,8 +108,18 @@ export function codeTool({
100108
let result: ToolCallResult;
101109
const startTime = Date.now();
102110

103-
logger.debug('Executing code in local Deno environment');
104-
result = await localDenoHandler({ reqContext, args });
111+
if (codeExecutionMode === 'remote-sandbox') {
112+
logger.debug('Executing code in remote sandbox');
113+
result = await remoteSandboxHandler({
114+
reqContext,
115+
args,
116+
endpoint: sandboxEndpoint,
117+
fetcher: sandboxFetcher,
118+
});
119+
} else {
120+
logger.debug('Executing code in local Deno environment');
121+
result = await localDenoHandler({ reqContext, args });
122+
}
105123

106124
logger.info(
107125
{

packages/mcp-server/src/options.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@ export type McpOptions = {
2424
codeAllowedMethods?: string[] | undefined;
2525
codeBlockedMethods?: string[] | undefined;
2626
codeExecutionMode: McpCodeExecutionMode;
27+
sandboxEndpoint?: string | undefined;
28+
sandboxFetcher?: typeof fetch | undefined;
2729
customInstructionsPath?: string | undefined;
2830
};
2931

30-
export type McpCodeExecutionMode = 'local';
32+
export type McpCodeExecutionMode = 'local' | 'remote-sandbox';
3133

3234
export function parseCLIOptions(): CLIOptions {
3335
const opts = yargs(hideBin(process.argv))
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Custom code — not generated. Do not remove during regeneration.
2+
//
3+
// Runs the `execute` tool in a remote sandbox (coingecko/coingecko-mcp-sandbox)
4+
// instead of a local Deno subprocess. Serverless runtimes like Cloudflare
5+
// Workers cannot spawn subprocesses, so hosted deployments must use this mode.
6+
//
7+
// Wire contract (matches what coingecko-mcp-sandbox serves):
8+
// POST <endpoint>
9+
// headers: x-stainless-mcp-client-envs: JSON of env vars for the sandboxed SDK client
10+
// body: { project_name, code, intent, client_opts }
11+
// response: { is_error, result, log_lines, err_lines }
12+
13+
import { McpRequestContext, ToolCallResult, asErrorResult, asTextContentResult } from './types';
14+
import { WorkerInput, WorkerOutput } from './code-tool-types';
15+
import { readEnv } from './util';
16+
17+
export const remoteSandboxHandler = async ({
18+
reqContext,
19+
args,
20+
endpoint,
21+
fetcher,
22+
}: {
23+
reqContext: McpRequestContext;
24+
args: any;
25+
endpoint: string | undefined;
26+
fetcher?: typeof fetch | undefined;
27+
}): Promise<ToolCallResult> => {
28+
if (!endpoint) {
29+
return asErrorResult(
30+
"Code execution mode 'remote-sandbox' requires a sandbox endpoint. Set the sandboxEndpoint MCP option.",
31+
);
32+
}
33+
34+
const code = args.code as string;
35+
const intent = args.intent as string | undefined;
36+
const client = reqContext.client;
37+
const environment = readEnv('COINGECKO_ENVIRONMENT') || undefined;
38+
39+
// Credentials and base URL for the SDK client that runs inside the sandbox.
40+
// When a base URL or environment is configured via env vars, the sandbox
41+
// resolves the URL itself; otherwise forward this client's configured baseURL.
42+
const clientEnvs = {
43+
COINGECKO_PRO_API_KEY: readEnv('COINGECKO_PRO_API_KEY') ?? client.proAPIKey ?? undefined,
44+
COINGECKO_DEMO_API_KEY: readEnv('COINGECKO_DEMO_API_KEY') ?? client.demoAPIKey ?? undefined,
45+
COINGECKO_BASE_URL:
46+
readEnv('COINGECKO_BASE_URL') ?? environment ? undefined : client.baseURL ?? undefined,
47+
};
48+
// Env vars forwarded by an upstream proxy (request header) take precedence.
49+
const mergedClientEnvs = { ...clientEnvs, ...reqContext.upstreamClientEnvs };
50+
51+
const doFetch = fetcher ?? fetch;
52+
const res = await doFetch(endpoint, {
53+
method: 'POST',
54+
headers: {
55+
'Content-Type': 'application/json',
56+
'x-stainless-mcp-client-envs': JSON.stringify(mergedClientEnvs),
57+
},
58+
body: JSON.stringify({
59+
project_name: 'coingecko',
60+
code,
61+
intent,
62+
client_opts: { environment: environment as any },
63+
} satisfies WorkerInput),
64+
});
65+
66+
if (!res.ok) {
67+
throw new Error(
68+
`${res.status}: ${
69+
res.statusText
70+
} error when trying to contact the code execution sandbox. Details: ${await res.text()}`,
71+
);
72+
}
73+
74+
const { is_error, result, log_lines, err_lines } = (await res.json()) as WorkerOutput;
75+
const hasLogs = log_lines.length > 0 || err_lines.length > 0;
76+
const output = {
77+
result,
78+
...(log_lines.length > 0 && { log_lines }),
79+
...(err_lines.length > 0 && { err_lines }),
80+
};
81+
if (is_error) {
82+
return asErrorResult(typeof result === 'string' && !hasLogs ? result : JSON.stringify(output, null, 2));
83+
}
84+
return asTextContentResult(output);
85+
};

packages/mcp-server/src/server.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,8 @@ export function selectTools(options?: McpOptions): McpTool[] {
185185
codeTool({
186186
blockedMethods: blockedMethodsForCodeTool(options),
187187
codeExecutionMode: options?.codeExecutionMode ?? 'local',
188+
sandboxEndpoint: options?.sandboxEndpoint,
189+
sandboxFetcher: options?.sandboxFetcher,
188190
}),
189191
);
190192
}

0 commit comments

Comments
 (0)