|
| 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 | +}; |
0 commit comments