From c08f7c04267e000d51cfad22ec8337e456d20171 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 2 Feb 2026 12:34:56 +0000 Subject: [PATCH 01/50] fix(client): avoid memory leak with abort signals --- src/client.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/client.ts b/src/client.ts index ef1cc2fb..2ac1780d 100644 --- a/src/client.ts +++ b/src/client.ts @@ -599,9 +599,10 @@ export class ImageKit { controller: AbortController, ): Promise { const { signal, method, ...options } = init || {}; - if (signal) signal.addEventListener('abort', () => controller.abort()); + const abort = controller.abort.bind(controller); + if (signal) signal.addEventListener('abort', abort, { once: true }); - const timeout = setTimeout(() => controller.abort(), ms); + const timeout = setTimeout(abort, ms); const isReadableBody = ((globalThis as any).ReadableStream && options.body instanceof (globalThis as any).ReadableStream) || From 4b5fcbfd1188573ccd1cea40b8e4924a5e2051dc Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 2 Feb 2026 16:28:14 +0000 Subject: [PATCH 02/50] chore(client): do not parse responses with empty content-length --- src/internal/parse.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/internal/parse.ts b/src/internal/parse.ts index 10d1ca90..611cd86d 100644 --- a/src/internal/parse.ts +++ b/src/internal/parse.ts @@ -29,6 +29,12 @@ export async function defaultParseResponse(client: ImageKit, props: APIRespon const mediaType = contentType?.split(';')[0]?.trim(); const isJSON = mediaType?.includes('application/json') || mediaType?.endsWith('+json'); if (isJSON) { + const contentLength = response.headers.get('content-length'); + if (contentLength === '0') { + // if there is no content we can't do anything + return undefined as T; + } + const json = await response.json(); return json as T; } From 5f6c688f4f41df60d88fce94bc10cfdce4e29d78 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 3 Feb 2026 00:16:32 +0000 Subject: [PATCH 03/50] chore(internal): support oauth authorization code flow for MCP servers --- packages/mcp-server/package.json | 4 ++++ packages/mcp-server/src/headers.ts | 4 +++- packages/mcp-server/src/http.ts | 9 +++++---- packages/mcp-server/src/options.ts | 1 + 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json index 56c2098f..bdc0aa0f 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -34,10 +34,12 @@ "@cloudflare/cabidela": "^0.2.4", "@modelcontextprotocol/sdk": "^1.25.2", "@valtown/deno-http-worker": "^0.0.21", + "cookie-parser": "^1.4.6", "cors": "^2.8.5", "express": "^5.1.0", "fuse.js": "^7.1.0", "jq-web": "https://github.com/stainless-api/jq-web/releases/download/v0.8.8/jq-web.tar.gz", + "morgan": "^1.10.0", "qs": "^6.14.1", "typescript": "5.8.3", "yargs": "^17.7.2", @@ -50,9 +52,11 @@ }, "devDependencies": { "@anthropic-ai/mcpb": "^2.1.2", + "@types/cookie-parser": "^1.4.10", "@types/cors": "^2.8.19", "@types/express": "^5.0.3", "@types/jest": "^29.4.0", + "@types/morgan": "^1.9.10", "@types/qs": "^6.14.0", "@types/yargs": "^17.0.8", "@typescript-eslint/eslint-plugin": "8.31.1", diff --git a/packages/mcp-server/src/headers.ts b/packages/mcp-server/src/headers.ts index 40fd3090..1efed985 100644 --- a/packages/mcp-server/src/headers.ts +++ b/packages/mcp-server/src/headers.ts @@ -3,7 +3,7 @@ import { IncomingMessage } from 'node:http'; import { ClientOptions } from '@imagekit/nodejs'; -export const parseAuthHeaders = (req: IncomingMessage): Partial => { +export const parseAuthHeaders = (req: IncomingMessage, required?: boolean): Partial => { if (req.headers.authorization) { const scheme = req.headers.authorization.split(' ')[0]!; const value = req.headers.authorization.slice(scheme.length + 1); @@ -19,6 +19,8 @@ export const parseAuthHeaders = (req: IncomingMessage): Partial = 'Unsupported authorization scheme. Expected the "Authorization" header to be a supported scheme (Basic).', ); } + } else if (required) { + throw new Error('Missing required Authorization header; see WWW-Authenticate header for details.'); } const privateKey = diff --git a/packages/mcp-server/src/http.ts b/packages/mcp-server/src/http.ts index dcfeba6a..42eb5002 100644 --- a/packages/mcp-server/src/http.ts +++ b/packages/mcp-server/src/http.ts @@ -2,8 +2,8 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; - import express from 'express'; +import morgan from 'morgan'; import { McpOptions } from './options'; import { ClientOptions, initMcpServer, newMcpServer } from './server'; import { parseAuthHeaders } from './headers'; @@ -20,7 +20,7 @@ const newServer = ({ const server = newMcpServer(); try { - const authOptions = parseAuthHeaders(req); + const authOptions = parseAuthHeaders(req, false); initMcpServer({ server: server, clientOptions: { @@ -75,14 +75,15 @@ const del = async (req: express.Request, res: express.Response) => { export const streamableHTTPApp = ({ clientOptions = {}, - mcpOptions = {}, + mcpOptions, }: { clientOptions?: ClientOptions; - mcpOptions?: McpOptions; + mcpOptions: McpOptions; }): express.Express => { const app = express(); app.set('query parser', 'extended'); app.use(express.json()); + app.use(morgan('combined')); app.get('/', get); app.post('/', post({ clientOptions, mcpOptions })); diff --git a/packages/mcp-server/src/options.ts b/packages/mcp-server/src/options.ts index c66ad8ce..025280ec 100644 --- a/packages/mcp-server/src/options.ts +++ b/packages/mcp-server/src/options.ts @@ -35,6 +35,7 @@ export function parseCLIOptions(): CLIOptions { }) .option('port', { type: 'number', + default: 3000, description: 'Port to serve on if using http transport', }) .option('socket', { From 46c04e16c46bca7bc1b0383d151f027d7d918611 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 4 Feb 2026 18:40:04 +0000 Subject: [PATCH 04/50] chore(client): restructure abort controller binding --- src/client.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/client.ts b/src/client.ts index 2ac1780d..220345d2 100644 --- a/src/client.ts +++ b/src/client.ts @@ -599,7 +599,7 @@ export class ImageKit { controller: AbortController, ): Promise { const { signal, method, ...options } = init || {}; - const abort = controller.abort.bind(controller); + const abort = this._makeAbort(controller); if (signal) signal.addEventListener('abort', abort, { once: true }); const timeout = setTimeout(abort, ms); @@ -625,6 +625,7 @@ export class ImageKit { return await this.fetch.call(undefined, url, fetchOptions); } finally { clearTimeout(timeout); + if (signal) signal.removeEventListener('abort', abort); } } @@ -769,6 +770,12 @@ export class ImageKit { return headers.values; } + private _makeAbort(controller: AbortController) { + // note: we can't just inline this method inside `fetchWithTimeout()` because then the closure + // would capture all request options, and cause a memory leak. + return () => controller.abort(); + } + private buildBody({ options: { body, headers: rawHeaders } }: { options: FinalRequestOptions }): { bodyHeaders: HeadersLike; body: BodyInit | undefined; From ff4b97e40fb46ca0b4f3229074c3f614b045641c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 4 Feb 2026 19:12:32 +0000 Subject: [PATCH 05/50] chore(internal): refactor flag parsing for MCP servers and add debug flag --- packages/mcp-server/package.json | 1 + packages/mcp-server/src/http.ts | 27 ++++++++++++++++++++++----- packages/mcp-server/src/index.ts | 6 +++++- packages/mcp-server/src/options.ts | 28 +++++++++++++++------------- 4 files changed, 43 insertions(+), 19 deletions(-) diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json index bdc0aa0f..b289c973 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -40,6 +40,7 @@ "fuse.js": "^7.1.0", "jq-web": "https://github.com/stainless-api/jq-web/releases/download/v0.8.8/jq-web.tar.gz", "morgan": "^1.10.0", + "morgan-body": "^2.6.9", "qs": "^6.14.1", "typescript": "5.8.3", "yargs": "^17.7.2", diff --git a/packages/mcp-server/src/http.ts b/packages/mcp-server/src/http.ts index 42eb5002..3d728ecc 100644 --- a/packages/mcp-server/src/http.ts +++ b/packages/mcp-server/src/http.ts @@ -4,6 +4,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import express from 'express'; import morgan from 'morgan'; +import morganBody from 'morgan-body'; import { McpOptions } from './options'; import { ClientOptions, initMcpServer, newMcpServer } from './server'; import { parseAuthHeaders } from './headers'; @@ -76,14 +77,26 @@ const del = async (req: express.Request, res: express.Response) => { export const streamableHTTPApp = ({ clientOptions = {}, mcpOptions, + debug, }: { clientOptions?: ClientOptions; mcpOptions: McpOptions; + debug: boolean; }): express.Express => { const app = express(); app.set('query parser', 'extended'); app.use(express.json()); - app.use(morgan('combined')); + + if (debug) { + morganBody(app, { + logAllReqHeader: true, + logAllResHeader: true, + logRequestBody: true, + logResponseBody: true, + }); + } else { + app.use(morgan('combined')); + } app.get('/', get); app.post('/', post({ clientOptions, mcpOptions })); @@ -92,9 +105,13 @@ export const streamableHTTPApp = ({ return app; }; -export const launchStreamableHTTPServer = async (options: McpOptions, port: number | string | undefined) => { - const app = streamableHTTPApp({ mcpOptions: options }); - const server = app.listen(port); +export const launchStreamableHTTPServer = async (params: { + mcpOptions: McpOptions; + debug: boolean; + port: number | string | undefined; +}) => { + const app = streamableHTTPApp({ mcpOptions: params.mcpOptions, debug: params.debug }); + const server = app.listen(params.port); const address = server.address(); if (typeof address === 'string') { @@ -102,6 +119,6 @@ export const launchStreamableHTTPServer = async (options: McpOptions, port: numb } else if (address !== null) { console.error(`MCP Server running on streamable HTTP on port ${address.port}`); } else { - console.error(`MCP Server running on streamable HTTP on port ${port}`); + console.error(`MCP Server running on streamable HTTP on port ${params.port}`); } }; diff --git a/packages/mcp-server/src/index.ts b/packages/mcp-server/src/index.ts index 0f6dd426..d75968e3 100644 --- a/packages/mcp-server/src/index.ts +++ b/packages/mcp-server/src/index.ts @@ -21,7 +21,11 @@ async function main() { await launchStdioServer(); break; case 'http': - await launchStreamableHTTPServer(options, options.port ?? options.socket); + await launchStreamableHTTPServer({ + mcpOptions: options, + debug: options.debug, + port: options.port ?? options.socket, + }); break; } } diff --git a/packages/mcp-server/src/options.ts b/packages/mcp-server/src/options.ts index 025280ec..74380833 100644 --- a/packages/mcp-server/src/options.ts +++ b/packages/mcp-server/src/options.ts @@ -4,6 +4,7 @@ import { hideBin } from 'yargs/helpers'; import z from 'zod'; export type CLIOptions = McpOptions & { + debug: boolean; transport: 'stdio' | 'http'; port: number | undefined; socket: string | undefined; @@ -15,17 +16,24 @@ export type McpOptions = { export function parseCLIOptions(): CLIOptions { const opts = yargs(hideBin(process.argv)) - .option('tools', { + .option('debug', { type: 'boolean', description: 'Enable debug logging' }) + .option('no-tools', { type: 'string', array: true, choices: ['code', 'docs'], - description: 'Use dynamic tools or all tools', + description: 'Tools to explicitly disable', }) - .option('no-tools', { + .option('port', { + type: 'number', + default: 3000, + description: 'Port to serve on if using http transport', + }) + .option('socket', { type: 'string', description: 'Unix socket to serve on if using http transport' }) + .option('tools', { type: 'string', array: true, choices: ['code', 'docs'], - description: 'Do not use any dynamic or all tools', + description: 'Tools to explicitly enable', }) .option('transport', { type: 'string', @@ -33,15 +41,8 @@ export function parseCLIOptions(): CLIOptions { default: 'stdio', description: 'What transport to use; stdio for local servers or http for remote servers', }) - .option('port', { - type: 'number', - default: 3000, - description: 'Port to serve on if using http transport', - }) - .option('socket', { - type: 'string', - description: 'Unix socket to serve on if using http transport', - }) + .env('MCP_SERVER') + .version(true) .help(); const argv = opts.parseSync(); @@ -57,6 +58,7 @@ export function parseCLIOptions(): CLIOptions { return { ...(includeDocsTools !== undefined && { includeDocsTools }), + debug: !!argv.debug, transport, port: argv.port, socket: argv.socket, From cdce131dc17fba5469393a285ac536acd74742b2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 4 Feb 2026 19:33:03 +0000 Subject: [PATCH 06/50] feat(mcp): add initial server instructions Adds generated MCP server instructions, to help agents get easy tasks on the first try. --- packages/mcp-server/src/http.ts | 10 +++---- packages/mcp-server/src/server.ts | 48 +++++++++++++++++++++++++++---- packages/mcp-server/src/stdio.ts | 4 +-- 3 files changed, 49 insertions(+), 13 deletions(-) diff --git a/packages/mcp-server/src/http.ts b/packages/mcp-server/src/http.ts index 3d728ecc..1f851cb6 100644 --- a/packages/mcp-server/src/http.ts +++ b/packages/mcp-server/src/http.ts @@ -9,7 +9,7 @@ import { McpOptions } from './options'; import { ClientOptions, initMcpServer, newMcpServer } from './server'; import { parseAuthHeaders } from './headers'; -const newServer = ({ +const newServer = async ({ clientOptions, req, res, @@ -17,12 +17,12 @@ const newServer = ({ clientOptions: ClientOptions; req: express.Request; res: express.Response; -}): McpServer | null => { - const server = newMcpServer(); +}): Promise => { + const server = await newMcpServer(); try { const authOptions = parseAuthHeaders(req, false); - initMcpServer({ + await initMcpServer({ server: server, clientOptions: { ...clientOptions, @@ -46,7 +46,7 @@ const newServer = ({ const post = (options: { clientOptions: ClientOptions; mcpOptions: McpOptions }) => async (req: express.Request, res: express.Response) => { - const server = newServer({ ...options, req, res }); + const server = await newServer({ ...options, req, res }); // If we return null, we already set the authorization error. if (server === null) return; const transport = new StreamableHTTPServerTransport(); diff --git a/packages/mcp-server/src/server.ts b/packages/mcp-server/src/server.ts index 7b26259a..d1c7cbf6 100644 --- a/packages/mcp-server/src/server.ts +++ b/packages/mcp-server/src/server.ts @@ -17,23 +17,59 @@ import { HandlerFunction, McpTool } from './types'; export { McpOptions } from './options'; export { ClientOptions } from '@imagekit/nodejs'; -export const newMcpServer = () => +async function getInstructions() { + // This API key is optional; providing it allows the server to fetch instructions for unreleased versions. + const stainlessAPIKey = readEnv('STAINLESS_API_KEY'); + const response = await fetch( + readEnv('CODE_MODE_INSTRUCTIONS_URL') ?? 'https://api.stainless.com/api/ai/instructions/imagekit', + { + method: 'GET', + headers: { ...(stainlessAPIKey && { Authorization: stainlessAPIKey }) }, + }, + ); + + let instructions: string | undefined; + if (!response.ok) { + console.warn( + 'Warning: failed to retrieve MCP server instructions. Proceeding with default instructions...', + ); + + instructions = ` + This is the imagekit MCP server. You will use Code Mode to help the user perform + actions. You can use search_docs tool to learn about how to take action with this server. Then, + you will write TypeScript code using the execute tool take action. It is CRITICAL that you be + thoughtful and deliberate when executing code. Always try to entirely solve the problem in code + block: it can be as long as you need to get the job done! + `; + } + + instructions ??= ((await response.json()) as { instructions: string }).instructions; + instructions = ` + The current time in Unix timestamps is ${Date.now()}. + + ${instructions} + `; + + return instructions; +} + +export const newMcpServer = async () => new McpServer( { name: 'imagekit_nodejs_api', version: '7.3.0', }, - { capabilities: { tools: {}, logging: {} } }, + { + instructions: await getInstructions(), + capabilities: { tools: {}, logging: {} }, + }, ); -// Create server instance -export const server = newMcpServer(); - /** * Initializes the provided MCP Server with the given tools and handlers. * If not provided, the default client, tools and handlers will be used. */ -export function initMcpServer(params: { +export async function initMcpServer(params: { server: Server | McpServer; clientOptions?: ClientOptions; mcpOptions?: McpOptions; diff --git a/packages/mcp-server/src/stdio.ts b/packages/mcp-server/src/stdio.ts index f07696f3..47aeb0c9 100644 --- a/packages/mcp-server/src/stdio.ts +++ b/packages/mcp-server/src/stdio.ts @@ -2,9 +2,9 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' import { initMcpServer, newMcpServer } from './server'; export const launchStdioServer = async () => { - const server = newMcpServer(); + const server = await newMcpServer(); - initMcpServer({ server }); + await initMcpServer({ server }); const transport = new StdioServerTransport(); await server.connect(transport); From 0738e8884a59ddac579fab6a65e0221fdff4247c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 5 Feb 2026 14:11:33 +0000 Subject: [PATCH 07/50] fix(client): avoid removing abort listener too early --- src/client.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/client.ts b/src/client.ts index 220345d2..453f5f99 100644 --- a/src/client.ts +++ b/src/client.ts @@ -625,7 +625,6 @@ export class ImageKit { return await this.fetch.call(undefined, url, fetchOptions); } finally { clearTimeout(timeout); - if (signal) signal.removeEventListener('abort', abort); } } From 83d1174751241a66748b9d0f4b2b92f37715d4ad Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 6 Feb 2026 16:56:54 +0000 Subject: [PATCH 08/50] chore(internal): add health check to MCP server when running in HTTP mode --- packages/mcp-server/src/http.ts | 3 +++ packages/mcp-server/src/server.ts | 8 ++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/mcp-server/src/http.ts b/packages/mcp-server/src/http.ts index 1f851cb6..b2031368 100644 --- a/packages/mcp-server/src/http.ts +++ b/packages/mcp-server/src/http.ts @@ -98,6 +98,9 @@ export const streamableHTTPApp = ({ app.use(morgan('combined')); } + app.get('/health', async (req: express.Request, res: express.Response) => { + res.status(200).send('OK'); + }); app.get('/', get); app.post('/', post({ clientOptions, mcpOptions })); app.delete('/', del); diff --git a/packages/mcp-server/src/server.ts b/packages/mcp-server/src/server.ts index d1c7cbf6..21026821 100644 --- a/packages/mcp-server/src/server.ts +++ b/packages/mcp-server/src/server.ts @@ -36,10 +36,10 @@ async function getInstructions() { instructions = ` This is the imagekit MCP server. You will use Code Mode to help the user perform - actions. You can use search_docs tool to learn about how to take action with this server. Then, - you will write TypeScript code using the execute tool take action. It is CRITICAL that you be - thoughtful and deliberate when executing code. Always try to entirely solve the problem in code - block: it can be as long as you need to get the job done! + actions. You can use search_docs tool to learn about how to take action with this server. Then, + you will write TypeScript code using the execute tool take action. It is CRITICAL that you be + thoughtful and deliberate when executing code. Always try to entirely solve the problem in code + block: it can be as long as you need to get the job done! `; } From 61a5d8863e4fcb692d187bb0a7b44e1788faf8ee Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 6 Feb 2026 21:06:00 +0000 Subject: [PATCH 09/50] chore(internal): upgrade hono --- packages/mcp-server/cloudflare-worker/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mcp-server/cloudflare-worker/package.json b/packages/mcp-server/cloudflare-worker/package.json index dfdc2bbc..468245d4 100644 --- a/packages/mcp-server/cloudflare-worker/package.json +++ b/packages/mcp-server/cloudflare-worker/package.json @@ -20,7 +20,7 @@ "@cloudflare/workers-oauth-provider": "^0.0.5", "@modelcontextprotocol/sdk": "^1.25.2", "agents": "^0.0.88", - "hono": "^4.11.4", + "hono": "^4.11.7", "@imagekit/api-mcp": "latest", "zod": "^3.24.4" } From 90eae18e29708d7596a6e783cad196c9a4f75f39 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 9 Feb 2026 18:33:51 +0000 Subject: [PATCH 10/50] chore(internal): always generate MCP server dockerfiles and upgrade associated dependencies --- .dockerignore | 59 ++++++++++++++++++++++++++++ packages/mcp-server/Dockerfile | 71 ++++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 .dockerignore create mode 100644 packages/mcp-server/Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..12ff1e64 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,59 @@ +# Dependencies +node_modules/ +**/node_modules/ + +# Build outputs +dist/ +**/dist/ + +# Git +.git/ +.gitignore + +# CI/CD +.github/ +.gitlab-ci.yml +.travis.yml + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Testing +test/ +tests/ +__tests__/ +*.test.js +*.spec.js +coverage/ +.nyc_output/ + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Environment +.env +.env.* + +# Temporary files +*.tmp +*.temp +.cache/ + +# Examples and scripts +examples/ +bin/ + +# Other packages (we only need mcp-server) +packages/*/ +!packages/mcp-server/ diff --git a/packages/mcp-server/Dockerfile b/packages/mcp-server/Dockerfile new file mode 100644 index 00000000..3fe3e5a4 --- /dev/null +++ b/packages/mcp-server/Dockerfile @@ -0,0 +1,71 @@ +# Dockerfile for Image Kit MCP Server +# +# This Dockerfile builds a Docker image for the MCP Server. +# +# To build the image locally: +# docker build -f packages/mcp-server/Dockerfile -t @imagekit/api-mcp:local . +# +# To run the image: +# docker run -i @imagekit/api-mcp:local [OPTIONS] +# +# Common options: +# --tool= Include specific tools +# --resource= Include tools for specific resources +# --operation=read|write Filter by operation type +# --client= Set client compatibility (e.g., claude, cursor) +# --transport= Set transport type (stdio or http) +# +# For a full list of options: +# docker run -i @imagekit/api-mcp:local --help +# +# Note: The MCP server uses stdio transport by default. Docker's -i flag +# enables interactive mode, allowing the container to communicate over stdin/stdout. + +# Build stage +FROM node:24-alpine AS builder + +# Install bash for build script +RUN apk add --no-cache bash openssl + +# Set working directory +WORKDIR /build + +# Copy entire repository +COPY . . + +# Install all dependencies and build everything +RUN yarn install --frozen-lockfile && \ + yarn build + +# Production stage +FROM node:24-alpine + +# Add non-root user +RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001 + +# Set working directory +WORKDIR /app + +# Copy the built mcp-server dist directory +COPY --from=builder /build/packages/mcp-server/dist ./ + +# Copy node_modules from mcp-server (includes all production deps) +COPY --from=builder /build/packages/mcp-server/node_modules ./node_modules + +# Copy the built @imagekit/nodejs into node_modules +COPY --from=builder /build/dist ./node_modules/@imagekit/nodejs + +# Change ownership to nodejs user +RUN chown -R nodejs:nodejs /app + +# Switch to non-root user +USER nodejs + +# The MCP server uses stdio transport by default +# No exposed ports needed for stdio communication + +# Set the entrypoint to the MCP server +ENTRYPOINT ["node", "index.js"] + +# Allow passing arguments to the MCP server +CMD [] From 4a861827d463d2b6e9812a4aa58d2df14cb356bf Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 9 Feb 2026 20:26:57 +0000 Subject: [PATCH 11/50] chore(internal): allow basic filtering of methods allowed for MCP code mode --- packages/mcp-server/src/code-tool.ts | 21 +- packages/mcp-server/src/http.ts | 3 + packages/mcp-server/src/index.ts | 2 +- packages/mcp-server/src/methods.ts | 368 +++++++++++++++++++++++++++ packages/mcp-server/src/options.ts | 23 ++ packages/mcp-server/src/server.ts | 7 +- packages/mcp-server/src/stdio.ts | 5 +- 7 files changed, 424 insertions(+), 5 deletions(-) create mode 100644 packages/mcp-server/src/methods.ts diff --git a/packages/mcp-server/src/code-tool.ts b/packages/mcp-server/src/code-tool.ts index 220b5c74..d92db0d3 100644 --- a/packages/mcp-server/src/code-tool.ts +++ b/packages/mcp-server/src/code-tool.ts @@ -4,6 +4,7 @@ import { McpTool, Metadata, ToolCallResult, asErrorResult, asTextContentResult } import { Tool } from '@modelcontextprotocol/sdk/types.js'; import { readEnv, requireValue } from './server'; import { WorkerInput, WorkerOutput } from './code-tool-types'; +import { SdkMethod } from './methods'; import { ImageKit } from '@imagekit/nodejs'; const prompt = `Runs JavaScript code to interact with the Image Kit API. @@ -35,7 +36,7 @@ Variables will not persist between calls, so make sure to return or log any data * * @param endpoints - The endpoints to include in the list. */ -export function codeTool(): McpTool { +export function codeTool(params: { blockedMethods: SdkMethod[] | undefined }): McpTool { const metadata: Metadata = { resource: 'all', operation: 'write', tags: [] }; const tool: Tool = { name: 'execute', @@ -59,6 +60,24 @@ export function codeTool(): McpTool { const code = args.code as string; const intent = args.intent as string | undefined; + // Do very basic blocking of code that includes forbidden method names. + // + // WARNING: This is not secure against obfuscation and other evasion methods. If + // stronger security blocks are required, then these should be enforced in the downstream + // API (e.g., by having users call the MCP server with API keys with limited permissions). + if (params.blockedMethods) { + const blockedMatches = params.blockedMethods.filter((method) => + code.includes(method.fullyQualifiedName), + ); + if (blockedMatches.length > 0) { + return asErrorResult( + `The following methods have been blocked by the MCP server and cannot be used in code execution: ${blockedMatches + .map((m) => m.fullyQualifiedName) + .join(', ')}`, + ); + } + } + // this is not required, but passing a Stainless API key for the matching project_name // will allow you to run code-mode queries against non-published versions of your SDK. const stainlessAPIKey = readEnv('STAINLESS_API_KEY'); diff --git a/packages/mcp-server/src/http.ts b/packages/mcp-server/src/http.ts index b2031368..8ca827c2 100644 --- a/packages/mcp-server/src/http.ts +++ b/packages/mcp-server/src/http.ts @@ -11,10 +11,12 @@ import { parseAuthHeaders } from './headers'; const newServer = async ({ clientOptions, + mcpOptions, req, res, }: { clientOptions: ClientOptions; + mcpOptions: McpOptions; req: express.Request; res: express.Response; }): Promise => { @@ -24,6 +26,7 @@ const newServer = async ({ const authOptions = parseAuthHeaders(req, false); await initMcpServer({ server: server, + mcpOptions: mcpOptions, clientOptions: { ...clientOptions, ...authOptions, diff --git a/packages/mcp-server/src/index.ts b/packages/mcp-server/src/index.ts index d75968e3..003a7655 100644 --- a/packages/mcp-server/src/index.ts +++ b/packages/mcp-server/src/index.ts @@ -18,7 +18,7 @@ async function main() { switch (options.transport) { case 'stdio': - await launchStdioServer(); + await launchStdioServer(options); break; case 'http': await launchStreamableHTTPServer({ diff --git a/packages/mcp-server/src/methods.ts b/packages/mcp-server/src/methods.ts new file mode 100644 index 00000000..4757e2b0 --- /dev/null +++ b/packages/mcp-server/src/methods.ts @@ -0,0 +1,368 @@ +import { McpOptions } from './options'; + +export type SdkMethod = { + clientCallName: string; + fullyQualifiedName: string; + httpMethod?: 'get' | 'post' | 'put' | 'patch' | 'delete' | 'query'; + httpPath?: string; +}; + +export const sdkMethods: SdkMethod[] = [ + { + clientCallName: 'client.customMetadataFields.create', + fullyQualifiedName: 'customMetadataFields.create', + httpMethod: 'post', + httpPath: '/v1/customMetadataFields', + }, + { + clientCallName: 'client.customMetadataFields.update', + fullyQualifiedName: 'customMetadataFields.update', + httpMethod: 'patch', + httpPath: '/v1/customMetadataFields/{id}', + }, + { + clientCallName: 'client.customMetadataFields.list', + fullyQualifiedName: 'customMetadataFields.list', + httpMethod: 'get', + httpPath: '/v1/customMetadataFields', + }, + { + clientCallName: 'client.customMetadataFields.delete', + fullyQualifiedName: 'customMetadataFields.delete', + httpMethod: 'delete', + httpPath: '/v1/customMetadataFields/{id}', + }, + { + clientCallName: 'client.files.update', + fullyQualifiedName: 'files.update', + httpMethod: 'patch', + httpPath: '/v1/files/{fileId}/details', + }, + { + clientCallName: 'client.files.delete', + fullyQualifiedName: 'files.delete', + httpMethod: 'delete', + httpPath: '/v1/files/{fileId}', + }, + { + clientCallName: 'client.files.copy', + fullyQualifiedName: 'files.copy', + httpMethod: 'post', + httpPath: '/v1/files/copy', + }, + { + clientCallName: 'client.files.get', + fullyQualifiedName: 'files.get', + httpMethod: 'get', + httpPath: '/v1/files/{fileId}/details', + }, + { + clientCallName: 'client.files.move', + fullyQualifiedName: 'files.move', + httpMethod: 'post', + httpPath: '/v1/files/move', + }, + { + clientCallName: 'client.files.rename', + fullyQualifiedName: 'files.rename', + httpMethod: 'put', + httpPath: '/v1/files/rename', + }, + { + clientCallName: 'client.files.upload', + fullyQualifiedName: 'files.upload', + httpMethod: 'post', + httpPath: '/api/v1/files/upload', + }, + { + clientCallName: 'client.files.bulk.delete', + fullyQualifiedName: 'files.bulk.delete', + httpMethod: 'post', + httpPath: '/v1/files/batch/deleteByFileIds', + }, + { + clientCallName: 'client.files.bulk.addTags', + fullyQualifiedName: 'files.bulk.addTags', + httpMethod: 'post', + httpPath: '/v1/files/addTags', + }, + { + clientCallName: 'client.files.bulk.removeAITags', + fullyQualifiedName: 'files.bulk.removeAITags', + httpMethod: 'post', + httpPath: '/v1/files/removeAITags', + }, + { + clientCallName: 'client.files.bulk.removeTags', + fullyQualifiedName: 'files.bulk.removeTags', + httpMethod: 'post', + httpPath: '/v1/files/removeTags', + }, + { + clientCallName: 'client.files.versions.list', + fullyQualifiedName: 'files.versions.list', + httpMethod: 'get', + httpPath: '/v1/files/{fileId}/versions', + }, + { + clientCallName: 'client.files.versions.delete', + fullyQualifiedName: 'files.versions.delete', + httpMethod: 'delete', + httpPath: '/v1/files/{fileId}/versions/{versionId}', + }, + { + clientCallName: 'client.files.versions.get', + fullyQualifiedName: 'files.versions.get', + httpMethod: 'get', + httpPath: '/v1/files/{fileId}/versions/{versionId}', + }, + { + clientCallName: 'client.files.versions.restore', + fullyQualifiedName: 'files.versions.restore', + httpMethod: 'put', + httpPath: '/v1/files/{fileId}/versions/{versionId}/restore', + }, + { + clientCallName: 'client.files.metadata.get', + fullyQualifiedName: 'files.metadata.get', + httpMethod: 'get', + httpPath: '/v1/files/{fileId}/metadata', + }, + { + clientCallName: 'client.files.metadata.getFromURL', + fullyQualifiedName: 'files.metadata.getFromURL', + httpMethod: 'get', + httpPath: '/v1/metadata', + }, + { + clientCallName: 'client.savedExtensions.create', + fullyQualifiedName: 'savedExtensions.create', + httpMethod: 'post', + httpPath: '/v1/saved-extensions', + }, + { + clientCallName: 'client.savedExtensions.update', + fullyQualifiedName: 'savedExtensions.update', + httpMethod: 'patch', + httpPath: '/v1/saved-extensions/{id}', + }, + { + clientCallName: 'client.savedExtensions.list', + fullyQualifiedName: 'savedExtensions.list', + httpMethod: 'get', + httpPath: '/v1/saved-extensions', + }, + { + clientCallName: 'client.savedExtensions.delete', + fullyQualifiedName: 'savedExtensions.delete', + httpMethod: 'delete', + httpPath: '/v1/saved-extensions/{id}', + }, + { + clientCallName: 'client.savedExtensions.get', + fullyQualifiedName: 'savedExtensions.get', + httpMethod: 'get', + httpPath: '/v1/saved-extensions/{id}', + }, + { + clientCallName: 'client.assets.list', + fullyQualifiedName: 'assets.list', + httpMethod: 'get', + httpPath: '/v1/files', + }, + { + clientCallName: 'client.cache.invalidation.create', + fullyQualifiedName: 'cache.invalidation.create', + httpMethod: 'post', + httpPath: '/v1/files/purge', + }, + { + clientCallName: 'client.cache.invalidation.get', + fullyQualifiedName: 'cache.invalidation.get', + httpMethod: 'get', + httpPath: '/v1/files/purge/{requestId}', + }, + { + clientCallName: 'client.folders.create', + fullyQualifiedName: 'folders.create', + httpMethod: 'post', + httpPath: '/v1/folder', + }, + { + clientCallName: 'client.folders.delete', + fullyQualifiedName: 'folders.delete', + httpMethod: 'delete', + httpPath: '/v1/folder', + }, + { + clientCallName: 'client.folders.copy', + fullyQualifiedName: 'folders.copy', + httpMethod: 'post', + httpPath: '/v1/bulkJobs/copyFolder', + }, + { + clientCallName: 'client.folders.move', + fullyQualifiedName: 'folders.move', + httpMethod: 'post', + httpPath: '/v1/bulkJobs/moveFolder', + }, + { + clientCallName: 'client.folders.rename', + fullyQualifiedName: 'folders.rename', + httpMethod: 'post', + httpPath: '/v1/bulkJobs/renameFolder', + }, + { + clientCallName: 'client.folders.job.get', + fullyQualifiedName: 'folders.job.get', + httpMethod: 'get', + httpPath: '/v1/bulkJobs/{jobId}', + }, + { + clientCallName: 'client.accounts.usage.get', + fullyQualifiedName: 'accounts.usage.get', + httpMethod: 'get', + httpPath: '/v1/accounts/usage', + }, + { + clientCallName: 'client.accounts.origins.create', + fullyQualifiedName: 'accounts.origins.create', + httpMethod: 'post', + httpPath: '/v1/accounts/origins', + }, + { + clientCallName: 'client.accounts.origins.update', + fullyQualifiedName: 'accounts.origins.update', + httpMethod: 'put', + httpPath: '/v1/accounts/origins/{id}', + }, + { + clientCallName: 'client.accounts.origins.list', + fullyQualifiedName: 'accounts.origins.list', + httpMethod: 'get', + httpPath: '/v1/accounts/origins', + }, + { + clientCallName: 'client.accounts.origins.delete', + fullyQualifiedName: 'accounts.origins.delete', + httpMethod: 'delete', + httpPath: '/v1/accounts/origins/{id}', + }, + { + clientCallName: 'client.accounts.origins.get', + fullyQualifiedName: 'accounts.origins.get', + httpMethod: 'get', + httpPath: '/v1/accounts/origins/{id}', + }, + { + clientCallName: 'client.accounts.urlEndpoints.create', + fullyQualifiedName: 'accounts.urlEndpoints.create', + httpMethod: 'post', + httpPath: '/v1/accounts/url-endpoints', + }, + { + clientCallName: 'client.accounts.urlEndpoints.update', + fullyQualifiedName: 'accounts.urlEndpoints.update', + httpMethod: 'put', + httpPath: '/v1/accounts/url-endpoints/{id}', + }, + { + clientCallName: 'client.accounts.urlEndpoints.list', + fullyQualifiedName: 'accounts.urlEndpoints.list', + httpMethod: 'get', + httpPath: '/v1/accounts/url-endpoints', + }, + { + clientCallName: 'client.accounts.urlEndpoints.delete', + fullyQualifiedName: 'accounts.urlEndpoints.delete', + httpMethod: 'delete', + httpPath: '/v1/accounts/url-endpoints/{id}', + }, + { + clientCallName: 'client.accounts.urlEndpoints.get', + fullyQualifiedName: 'accounts.urlEndpoints.get', + httpMethod: 'get', + httpPath: '/v1/accounts/url-endpoints/{id}', + }, + { + clientCallName: 'client.beta.v2.files.upload', + fullyQualifiedName: 'beta.v2.files.upload', + httpMethod: 'post', + httpPath: '/api/v2/files/upload', + }, + { clientCallName: 'client.webhooks.unsafeUnwrap', fullyQualifiedName: 'webhooks.unsafeUnwrap' }, + { clientCallName: 'client.webhooks.unwrap', fullyQualifiedName: 'webhooks.unwrap' }, +]; + +function allowedMethodsForCodeTool(options: McpOptions | undefined): SdkMethod[] | undefined { + if (!options) { + return undefined; + } + + let allowedMethods: SdkMethod[]; + + if (options.codeAllowHttpGets || options.codeAllowedMethods) { + // Start with nothing allowed and then add into it from options + let allowedMethodsSet = new Set(); + + if (options.codeAllowHttpGets) { + // Add all methods that map to an HTTP GET + sdkMethods + .filter((method) => method.httpMethod === 'get') + .forEach((method) => allowedMethodsSet.add(method)); + } + + if (options.codeAllowedMethods) { + // Add all methods that match any of the allowed regexps + const allowedRegexps = options.codeAllowedMethods.map((pattern) => { + try { + return new RegExp(pattern); + } catch (e) { + throw new Error( + `Invalid regex pattern for allowed method: "${pattern}": ${e instanceof Error ? e.message : e}`, + ); + } + }); + + sdkMethods + .filter((method) => allowedRegexps.some((regexp) => regexp.test(method.fullyQualifiedName))) + .forEach((method) => allowedMethodsSet.add(method)); + } + + allowedMethods = Array.from(allowedMethodsSet); + } else { + // Start with everything allowed + allowedMethods = [...sdkMethods]; + } + + if (options.codeBlockedMethods) { + // Filter down based on blocked regexps + const blockedRegexps = options.codeBlockedMethods.map((pattern) => { + try { + return new RegExp(pattern); + } catch (e) { + throw new Error( + `Invalid regex pattern for blocked method: "${pattern}": ${e instanceof Error ? e.message : e}`, + ); + } + }); + + allowedMethods = allowedMethods.filter( + (method) => !blockedRegexps.some((regexp) => regexp.test(method.fullyQualifiedName)), + ); + } + + return allowedMethods; +} + +export function blockedMethodsForCodeTool(options: McpOptions | undefined): SdkMethod[] | undefined { + const allowedMethods = allowedMethodsForCodeTool(options); + if (!allowedMethods) { + return undefined; + } + + const allowedSet = new Set(allowedMethods.map((method) => method.fullyQualifiedName)); + + // Return any methods that are not explicitly allowed + return sdkMethods.filter((method) => !allowedSet.has(method.fullyQualifiedName)); +} diff --git a/packages/mcp-server/src/options.ts b/packages/mcp-server/src/options.ts index 74380833..92d1b074 100644 --- a/packages/mcp-server/src/options.ts +++ b/packages/mcp-server/src/options.ts @@ -12,10 +12,30 @@ export type CLIOptions = McpOptions & { export type McpOptions = { includeDocsTools?: boolean | undefined; + codeAllowHttpGets?: boolean | undefined; + codeAllowedMethods?: string[] | undefined; + codeBlockedMethods?: string[] | undefined; }; export function parseCLIOptions(): CLIOptions { const opts = yargs(hideBin(process.argv)) + .option('code-allow-http-gets', { + type: 'boolean', + description: + 'Allow all code tool methods that map to HTTP GET operations. If all code-allow-* flags are unset, then everything is allowed.', + }) + .option('code-allowed-methods', { + type: 'string', + array: true, + description: + 'Methods to explicitly allow for code tool. Evaluated as regular expressions against method fully qualified names. If all code-allow-* flags are unset, then everything is allowed.', + }) + .option('code-blocked-methods', { + type: 'string', + array: true, + description: + 'Methods to explicitly block for code tool. Evaluated as regular expressions against method fully qualified names. If all code-allow-* flags are unset, then everything is allowed.', + }) .option('debug', { type: 'boolean', description: 'Enable debug logging' }) .option('no-tools', { type: 'string', @@ -59,6 +79,9 @@ export function parseCLIOptions(): CLIOptions { return { ...(includeDocsTools !== undefined && { includeDocsTools }), debug: !!argv.debug, + codeAllowHttpGets: argv.codeAllowHttpGets, + codeAllowedMethods: argv.codeAllowedMethods, + codeBlockedMethods: argv.codeBlockedMethods, transport, port: argv.port, socket: argv.socket, diff --git a/packages/mcp-server/src/server.ts b/packages/mcp-server/src/server.ts index 21026821..4e87e0eb 100644 --- a/packages/mcp-server/src/server.ts +++ b/packages/mcp-server/src/server.ts @@ -12,6 +12,7 @@ import ImageKit from '@imagekit/nodejs'; import { codeTool } from './code-tool'; import docsSearchTool from './docs-search-tool'; import { McpOptions } from './options'; +import { blockedMethodsForCodeTool } from './methods'; import { HandlerFunction, McpTool } from './types'; export { McpOptions } from './options'; @@ -147,7 +148,11 @@ export async function initMcpServer(params: { * Selects the tools to include in the MCP Server based on the provided options. */ export function selectTools(options?: McpOptions): McpTool[] { - const includedTools = [codeTool()]; + const includedTools = [ + codeTool({ + blockedMethods: blockedMethodsForCodeTool(options), + }), + ]; if (options?.includeDocsTools ?? true) { includedTools.push(docsSearchTool); } diff --git a/packages/mcp-server/src/stdio.ts b/packages/mcp-server/src/stdio.ts index 47aeb0c9..57b99126 100644 --- a/packages/mcp-server/src/stdio.ts +++ b/packages/mcp-server/src/stdio.ts @@ -1,10 +1,11 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { McpOptions } from './options'; import { initMcpServer, newMcpServer } from './server'; -export const launchStdioServer = async () => { +export const launchStdioServer = async (mcpOptions: McpOptions) => { const server = await newMcpServer(); - await initMcpServer({ server }); + await initMcpServer({ server, mcpOptions }); const transport = new StdioServerTransport(); await server.connect(transport); From 7cd398067ad0736b67bfb3d8ace58d15a94c1fd2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 14:12:05 +0000 Subject: [PATCH 12/50] chore(internal): avoid type checking errors with ts-reset --- src/client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.ts b/src/client.ts index 453f5f99..bb01dbea 100644 --- a/src/client.ts +++ b/src/client.ts @@ -558,7 +558,7 @@ export class ImageKit { loggerFor(this).info(`${responseInfo} - ${retryMessage}`); const errText = await response.text().catch((err: any) => castToError(err).message); - const errJSON = safeJSON(errText); + const errJSON = safeJSON(errText) as any; const errMessage = errJSON ? undefined : errText; loggerFor(this).debug( From bf91d7300f134a716038b00bbdcf0cd5932176ea Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 13 Feb 2026 21:36:43 +0000 Subject: [PATCH 13/50] chore(mcp): forward STAINLESS_API_KEY to docs search endpoint --- packages/mcp-server/src/docs-search-tool.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/mcp-server/src/docs-search-tool.ts b/packages/mcp-server/src/docs-search-tool.ts index 9af38ab5..be3e4298 100644 --- a/packages/mcp-server/src/docs-search-tool.ts +++ b/packages/mcp-server/src/docs-search-tool.ts @@ -1,6 +1,7 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. import { Metadata, asTextContentResult } from './types'; +import { readEnv } from './server'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; @@ -45,7 +46,12 @@ const docsSearchURL = export const handler = async (_: unknown, args: Record | undefined) => { const body = args as any; const query = new URLSearchParams(body).toString(); - const result = await fetch(`${docsSearchURL}?${query}`); + const stainlessAPIKey = readEnv('STAINLESS_API_KEY'); + const result = await fetch(`${docsSearchURL}?${query}`, { + headers: { + ...(stainlessAPIKey && { Authorization: stainlessAPIKey }), + }, + }); if (!result.ok) { throw new Error( From b2e0a75d79757596569d0277467ccad531d49bdd Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 13 Feb 2026 23:13:16 +0000 Subject: [PATCH 14/50] chore(internal): improve layout of generated MCP server files --- .../mcp-server/src/{headers.ts => auth.ts} | 0 packages/mcp-server/src/code-tool.ts | 2 +- packages/mcp-server/src/docs-search-tool.ts | 2 +- packages/mcp-server/src/http.ts | 5 ++-- packages/mcp-server/src/methods.ts | 2 ++ packages/mcp-server/src/options.ts | 2 ++ packages/mcp-server/src/server.ts | 28 +------------------ packages/mcp-server/src/util.ts | 25 +++++++++++++++++ 8 files changed, 35 insertions(+), 31 deletions(-) rename packages/mcp-server/src/{headers.ts => auth.ts} (100%) create mode 100644 packages/mcp-server/src/util.ts diff --git a/packages/mcp-server/src/headers.ts b/packages/mcp-server/src/auth.ts similarity index 100% rename from packages/mcp-server/src/headers.ts rename to packages/mcp-server/src/auth.ts diff --git a/packages/mcp-server/src/code-tool.ts b/packages/mcp-server/src/code-tool.ts index d92db0d3..ae3c9134 100644 --- a/packages/mcp-server/src/code-tool.ts +++ b/packages/mcp-server/src/code-tool.ts @@ -2,7 +2,7 @@ import { McpTool, Metadata, ToolCallResult, asErrorResult, asTextContentResult } from './types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import { readEnv, requireValue } from './server'; +import { readEnv, requireValue } from './util'; import { WorkerInput, WorkerOutput } from './code-tool-types'; import { SdkMethod } from './methods'; import { ImageKit } from '@imagekit/nodejs'; diff --git a/packages/mcp-server/src/docs-search-tool.ts b/packages/mcp-server/src/docs-search-tool.ts index be3e4298..985c9b7f 100644 --- a/packages/mcp-server/src/docs-search-tool.ts +++ b/packages/mcp-server/src/docs-search-tool.ts @@ -1,7 +1,7 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. import { Metadata, asTextContentResult } from './types'; -import { readEnv } from './server'; +import { readEnv } from './util'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; diff --git a/packages/mcp-server/src/http.ts b/packages/mcp-server/src/http.ts index 8ca827c2..a422b0ff 100644 --- a/packages/mcp-server/src/http.ts +++ b/packages/mcp-server/src/http.ts @@ -2,12 +2,13 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import { ClientOptions } from '@imagekit/nodejs'; import express from 'express'; import morgan from 'morgan'; import morganBody from 'morgan-body'; +import { parseAuthHeaders } from './auth'; import { McpOptions } from './options'; -import { ClientOptions, initMcpServer, newMcpServer } from './server'; -import { parseAuthHeaders } from './headers'; +import { initMcpServer, newMcpServer } from './server'; const newServer = async ({ clientOptions, diff --git a/packages/mcp-server/src/methods.ts b/packages/mcp-server/src/methods.ts index 4757e2b0..a2a4f930 100644 --- a/packages/mcp-server/src/methods.ts +++ b/packages/mcp-server/src/methods.ts @@ -1,3 +1,5 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + import { McpOptions } from './options'; export type SdkMethod = { diff --git a/packages/mcp-server/src/options.ts b/packages/mcp-server/src/options.ts index 92d1b074..cfde21d0 100644 --- a/packages/mcp-server/src/options.ts +++ b/packages/mcp-server/src/options.ts @@ -1,3 +1,5 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + import qs from 'qs'; import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; diff --git a/packages/mcp-server/src/server.ts b/packages/mcp-server/src/server.ts index 4e87e0eb..9ddcc935 100644 --- a/packages/mcp-server/src/server.ts +++ b/packages/mcp-server/src/server.ts @@ -14,9 +14,7 @@ import docsSearchTool from './docs-search-tool'; import { McpOptions } from './options'; import { blockedMethodsForCodeTool } from './methods'; import { HandlerFunction, McpTool } from './types'; - -export { McpOptions } from './options'; -export { ClientOptions } from '@imagekit/nodejs'; +import { readEnv } from './util'; async function getInstructions() { // This API key is optional; providing it allows the server to fetch instructions for unreleased versions. @@ -169,27 +167,3 @@ export async function executeHandler( ) { return await handler(client, args || {}); } - -export const readEnv = (env: string): string | undefined => { - if (typeof (globalThis as any).process !== 'undefined') { - return (globalThis as any).process.env?.[env]?.trim(); - } else if (typeof (globalThis as any).Deno !== 'undefined') { - return (globalThis as any).Deno.env?.get?.(env)?.trim(); - } - return; -}; - -export const readEnvOrError = (env: string): string => { - let envValue = readEnv(env); - if (envValue === undefined) { - throw new Error(`Environment variable ${env} is not set`); - } - return envValue; -}; - -export const requireValue = (value: T | undefined, description: string): T => { - if (value === undefined) { - throw new Error(`Missing required value: ${description}`); - } - return value; -}; diff --git a/packages/mcp-server/src/util.ts b/packages/mcp-server/src/util.ts new file mode 100644 index 00000000..40ed5501 --- /dev/null +++ b/packages/mcp-server/src/util.ts @@ -0,0 +1,25 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +export const readEnv = (env: string): string | undefined => { + if (typeof (globalThis as any).process !== 'undefined') { + return (globalThis as any).process.env?.[env]?.trim(); + } else if (typeof (globalThis as any).Deno !== 'undefined') { + return (globalThis as any).Deno.env?.get?.(env)?.trim(); + } + return; +}; + +export const readEnvOrError = (env: string): string => { + let envValue = readEnv(env); + if (envValue === undefined) { + throw new Error(`Environment variable ${env} is not set`); + } + return envValue; +}; + +export const requireValue = (value: T | undefined, description: string): T => { + if (value === undefined) { + throw new Error(`Missing required value: ${description}`); + } + return value; +}; From d96f4832db9e8c16cbeae32f9a7eb46234bb64ed Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 17:37:05 +0000 Subject: [PATCH 15/50] chore(internal/client): fix form-urlencoded requests --- src/client.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/client.ts b/src/client.ts index bb01dbea..9b293fb3 100644 --- a/src/client.ts +++ b/src/client.ts @@ -807,6 +807,14 @@ export class ImageKit { (Symbol.iterator in body && 'next' in body && typeof body.next === 'function')) ) { return { bodyHeaders: undefined, body: Shims.ReadableStreamFrom(body as AsyncIterable) }; + } else if ( + typeof body === 'object' && + headers.values.get('content-type') === 'application/x-www-form-urlencoded' + ) { + return { + bodyHeaders: { 'content-type': 'application/x-www-form-urlencoded' }, + body: this.stringifyQuery(body as Record), + }; } else { return this.#encoder({ body, headers }); } From a72133c81d0f9ad9587793bb92c06963fce21e8e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 19:22:33 +0000 Subject: [PATCH 16/50] chore(internal): allow setting x-stainless-api-key header on mcp server requests --- packages/mcp-server/src/auth.ts | 17 ++++++++- packages/mcp-server/src/code-tool.ts | 33 ++++++++++------- packages/mcp-server/src/docs-search-tool.ts | 15 ++++---- packages/mcp-server/src/http.ts | 21 +++++++---- packages/mcp-server/src/options.ts | 9 +++++ packages/mcp-server/src/server.ts | 40 +++++++++++++-------- packages/mcp-server/src/stdio.ts | 4 +-- packages/mcp-server/src/types.ts | 16 ++++++--- 8 files changed, 109 insertions(+), 46 deletions(-) diff --git a/packages/mcp-server/src/auth.ts b/packages/mcp-server/src/auth.ts index 1efed985..085cac43 100644 --- a/packages/mcp-server/src/auth.ts +++ b/packages/mcp-server/src/auth.ts @@ -2,8 +2,9 @@ import { IncomingMessage } from 'node:http'; import { ClientOptions } from '@imagekit/nodejs'; +import { McpOptions } from './options'; -export const parseAuthHeaders = (req: IncomingMessage, required?: boolean): Partial => { +export const parseClientAuthHeaders = (req: IncomingMessage, required?: boolean): Partial => { if (req.headers.authorization) { const scheme = req.headers.authorization.split(' ')[0]!; const value = req.headers.authorization.slice(scheme.length + 1); @@ -33,3 +34,17 @@ export const parseAuthHeaders = (req: IncomingMessage, required?: boolean): Part : req.headers['x-optional-imagekit-ignores-this']; return { privateKey, password }; }; + +export const getStainlessApiKey = (req: IncomingMessage, mcpOptions: McpOptions): string | undefined => { + // Try to get the key from the x-stainless-api-key header + const headerKey = + Array.isArray(req.headers['x-stainless-api-key']) ? + req.headers['x-stainless-api-key'][0] + : req.headers['x-stainless-api-key']; + if (headerKey && typeof headerKey === 'string') { + return headerKey; + } + + // Fall back to value set in the mcpOptions (e.g. from environment variable), if provided + return mcpOptions.stainlessApiKey; +}; diff --git a/packages/mcp-server/src/code-tool.ts b/packages/mcp-server/src/code-tool.ts index ae3c9134..932583e7 100644 --- a/packages/mcp-server/src/code-tool.ts +++ b/packages/mcp-server/src/code-tool.ts @@ -1,11 +1,17 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { McpTool, Metadata, ToolCallResult, asErrorResult, asTextContentResult } from './types'; +import { + McpRequestContext, + McpTool, + Metadata, + ToolCallResult, + asErrorResult, + asTextContentResult, +} from './types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; import { readEnv, requireValue } from './util'; import { WorkerInput, WorkerOutput } from './code-tool-types'; import { SdkMethod } from './methods'; -import { ImageKit } from '@imagekit/nodejs'; const prompt = `Runs JavaScript code to interact with the Image Kit API. @@ -36,7 +42,7 @@ Variables will not persist between calls, so make sure to return or log any data * * @param endpoints - The endpoints to include in the list. */ -export function codeTool(params: { blockedMethods: SdkMethod[] | undefined }): McpTool { +export function codeTool({ blockedMethods }: { blockedMethods: SdkMethod[] | undefined }): McpTool { const metadata: Metadata = { resource: 'all', operation: 'write', tags: [] }; const tool: Tool = { name: 'execute', @@ -56,19 +62,24 @@ export function codeTool(params: { blockedMethods: SdkMethod[] | undefined }): M required: ['code'], }, }; - const handler = async (client: ImageKit, args: any): Promise => { + const handler = async ({ + reqContext, + args, + }: { + reqContext: McpRequestContext; + args: any; + }): Promise => { const code = args.code as string; const intent = args.intent as string | undefined; + const client = reqContext.client; // Do very basic blocking of code that includes forbidden method names. // // WARNING: This is not secure against obfuscation and other evasion methods. If // stronger security blocks are required, then these should be enforced in the downstream // API (e.g., by having users call the MCP server with API keys with limited permissions). - if (params.blockedMethods) { - const blockedMatches = params.blockedMethods.filter((method) => - code.includes(method.fullyQualifiedName), - ); + if (blockedMethods) { + const blockedMatches = blockedMethods.filter((method) => code.includes(method.fullyQualifiedName)); if (blockedMatches.length > 0) { return asErrorResult( `The following methods have been blocked by the MCP server and cannot be used in code execution: ${blockedMatches @@ -78,16 +89,14 @@ export function codeTool(params: { blockedMethods: SdkMethod[] | undefined }): M } } - // this is not required, but passing a Stainless API key for the matching project_name - // will allow you to run code-mode queries against non-published versions of your SDK. - const stainlessAPIKey = readEnv('STAINLESS_API_KEY'); const codeModeEndpoint = readEnv('CODE_MODE_ENDPOINT_URL') ?? 'https://api.stainless.com/api/ai/code-tool'; + // Setting a Stainless API key authenticates requests to the code tool endpoint. const res = await fetch(codeModeEndpoint, { method: 'POST', headers: { - ...(stainlessAPIKey && { Authorization: stainlessAPIKey }), + ...(reqContext.stainlessApiKey && { Authorization: reqContext.stainlessApiKey }), 'Content-Type': 'application/json', client_envs: JSON.stringify({ IMAGEKIT_PRIVATE_KEY: requireValue( diff --git a/packages/mcp-server/src/docs-search-tool.ts b/packages/mcp-server/src/docs-search-tool.ts index 985c9b7f..6d78bb6b 100644 --- a/packages/mcp-server/src/docs-search-tool.ts +++ b/packages/mcp-server/src/docs-search-tool.ts @@ -1,8 +1,6 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { Metadata, asTextContentResult } from './types'; -import { readEnv } from './util'; - +import { Metadata, McpRequestContext, asTextContentResult } from './types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; export const metadata: Metadata = { @@ -43,13 +41,18 @@ export const tool: Tool = { const docsSearchURL = process.env['DOCS_SEARCH_URL'] || 'https://api.stainless.com/api/projects/imagekit/docs/search'; -export const handler = async (_: unknown, args: Record | undefined) => { +export const handler = async ({ + reqContext, + args, +}: { + reqContext: McpRequestContext; + args: Record | undefined; +}) => { const body = args as any; const query = new URLSearchParams(body).toString(); - const stainlessAPIKey = readEnv('STAINLESS_API_KEY'); const result = await fetch(`${docsSearchURL}?${query}`, { headers: { - ...(stainlessAPIKey && { Authorization: stainlessAPIKey }), + ...(reqContext.stainlessApiKey && { Authorization: reqContext.stainlessApiKey }), }, }); diff --git a/packages/mcp-server/src/http.ts b/packages/mcp-server/src/http.ts index a422b0ff..4632176b 100644 --- a/packages/mcp-server/src/http.ts +++ b/packages/mcp-server/src/http.ts @@ -6,7 +6,7 @@ import { ClientOptions } from '@imagekit/nodejs'; import express from 'express'; import morgan from 'morgan'; import morganBody from 'morgan-body'; -import { parseAuthHeaders } from './auth'; +import { getStainlessApiKey, parseClientAuthHeaders } from './auth'; import { McpOptions } from './options'; import { initMcpServer, newMcpServer } from './server'; @@ -21,10 +21,12 @@ const newServer = async ({ req: express.Request; res: express.Response; }): Promise => { - const server = await newMcpServer(); + const stainlessApiKey = getStainlessApiKey(req, mcpOptions); + const server = await newMcpServer(stainlessApiKey); try { - const authOptions = parseAuthHeaders(req, false); + const authOptions = parseClientAuthHeaders(req, false); + await initMcpServer({ server: server, mcpOptions: mcpOptions, @@ -32,6 +34,7 @@ const newServer = async ({ ...clientOptions, ...authOptions, }, + stainlessApiKey: stainlessApiKey, }); } catch (error) { res.status(401).json({ @@ -112,13 +115,17 @@ export const streamableHTTPApp = ({ return app; }; -export const launchStreamableHTTPServer = async (params: { +export const launchStreamableHTTPServer = async ({ + mcpOptions, + debug, + port, +}: { mcpOptions: McpOptions; debug: boolean; port: number | string | undefined; }) => { - const app = streamableHTTPApp({ mcpOptions: params.mcpOptions, debug: params.debug }); - const server = app.listen(params.port); + const app = streamableHTTPApp({ mcpOptions, debug }); + const server = app.listen(port); const address = server.address(); if (typeof address === 'string') { @@ -126,6 +133,6 @@ export const launchStreamableHTTPServer = async (params: { } else if (address !== null) { console.error(`MCP Server running on streamable HTTP on port ${address.port}`); } else { - console.error(`MCP Server running on streamable HTTP on port ${params.port}`); + console.error(`MCP Server running on streamable HTTP on port ${port}`); } }; diff --git a/packages/mcp-server/src/options.ts b/packages/mcp-server/src/options.ts index cfde21d0..32a88713 100644 --- a/packages/mcp-server/src/options.ts +++ b/packages/mcp-server/src/options.ts @@ -4,6 +4,7 @@ import qs from 'qs'; import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; import z from 'zod'; +import { readEnv } from './util'; export type CLIOptions = McpOptions & { debug: boolean; @@ -14,6 +15,7 @@ export type CLIOptions = McpOptions & { export type McpOptions = { includeDocsTools?: boolean | undefined; + stainlessApiKey?: string | undefined; codeAllowHttpGets?: boolean | undefined; codeAllowedMethods?: string[] | undefined; codeBlockedMethods?: string[] | undefined; @@ -51,6 +53,12 @@ export function parseCLIOptions(): CLIOptions { description: 'Port to serve on if using http transport', }) .option('socket', { type: 'string', description: 'Unix socket to serve on if using http transport' }) + .option('stainless-api-key', { + type: 'string', + default: readEnv('STAINLESS_API_KEY'), + description: + 'API key for Stainless. Used to authenticate requests to Stainless-hosted tools endpoints.', + }) .option('tools', { type: 'string', array: true, @@ -81,6 +89,7 @@ export function parseCLIOptions(): CLIOptions { return { ...(includeDocsTools !== undefined && { includeDocsTools }), debug: !!argv.debug, + stainlessApiKey: argv.stainlessApiKey, codeAllowHttpGets: argv.codeAllowHttpGets, codeAllowedMethods: argv.codeAllowedMethods, codeBlockedMethods: argv.codeBlockedMethods, diff --git a/packages/mcp-server/src/server.ts b/packages/mcp-server/src/server.ts index 9ddcc935..c1600288 100644 --- a/packages/mcp-server/src/server.ts +++ b/packages/mcp-server/src/server.ts @@ -13,17 +13,17 @@ import { codeTool } from './code-tool'; import docsSearchTool from './docs-search-tool'; import { McpOptions } from './options'; import { blockedMethodsForCodeTool } from './methods'; -import { HandlerFunction, McpTool } from './types'; +import { HandlerFunction, McpRequestContext, ToolCallResult, McpTool } from './types'; import { readEnv } from './util'; -async function getInstructions() { - // This API key is optional; providing it allows the server to fetch instructions for unreleased versions. - const stainlessAPIKey = readEnv('STAINLESS_API_KEY'); +async function getInstructions(stainlessApiKey: string | undefined): Promise { + // Setting the stainless API key is optional, but may be required + // to authenticate requests to the Stainless API. const response = await fetch( readEnv('CODE_MODE_INSTRUCTIONS_URL') ?? 'https://api.stainless.com/api/ai/instructions/imagekit', { method: 'GET', - headers: { ...(stainlessAPIKey && { Authorization: stainlessAPIKey }) }, + headers: { ...(stainlessApiKey && { Authorization: stainlessApiKey }) }, }, ); @@ -52,14 +52,14 @@ async function getInstructions() { return instructions; } -export const newMcpServer = async () => +export const newMcpServer = async (stainlessApiKey: string | undefined) => new McpServer( { name: 'imagekit_nodejs_api', version: '7.3.0', }, { - instructions: await getInstructions(), + instructions: await getInstructions(stainlessApiKey), capabilities: { tools: {}, logging: {} }, }, ); @@ -72,6 +72,7 @@ export async function initMcpServer(params: { server: Server | McpServer; clientOptions?: ClientOptions; mcpOptions?: McpOptions; + stainlessApiKey?: string | undefined; }) { const server = params.server instanceof McpServer ? params.server.server : params.server; @@ -115,7 +116,14 @@ export async function initMcpServer(params: { throw new Error(`Unknown tool: ${name}`); } - return executeHandler(mcpTool.handler, client, args); + return executeHandler({ + handler: mcpTool.handler, + reqContext: { + client, + stainlessApiKey: params.stainlessApiKey ?? params.mcpOptions?.stainlessApiKey, + }, + args, + }); }); server.setRequestHandler(SetLevelRequestSchema, async (request) => { @@ -160,10 +168,14 @@ export function selectTools(options?: McpOptions): McpTool[] { /** * Runs the provided handler with the given client and arguments. */ -export async function executeHandler( - handler: HandlerFunction, - client: ImageKit, - args: Record | undefined, -) { - return await handler(client, args || {}); +export async function executeHandler({ + handler, + reqContext, + args, +}: { + handler: HandlerFunction; + reqContext: McpRequestContext; + args: Record | undefined; +}): Promise { + return await handler({ reqContext, args: args || {} }); } diff --git a/packages/mcp-server/src/stdio.ts b/packages/mcp-server/src/stdio.ts index 57b99126..ceccaed3 100644 --- a/packages/mcp-server/src/stdio.ts +++ b/packages/mcp-server/src/stdio.ts @@ -3,9 +3,9 @@ import { McpOptions } from './options'; import { initMcpServer, newMcpServer } from './server'; export const launchStdioServer = async (mcpOptions: McpOptions) => { - const server = await newMcpServer(); + const server = await newMcpServer(mcpOptions.stainlessApiKey); - await initMcpServer({ server, mcpOptions }); + await initMcpServer({ server, mcpOptions, stainlessApiKey: mcpOptions.stainlessApiKey }); const transport = new StdioServerTransport(); await server.connect(transport); diff --git a/packages/mcp-server/src/types.ts b/packages/mcp-server/src/types.ts index 6b307bec..afe343ee 100644 --- a/packages/mcp-server/src/types.ts +++ b/packages/mcp-server/src/types.ts @@ -42,10 +42,18 @@ export type ToolCallResult = { isError?: boolean; }; -export type HandlerFunction = ( - client: ImageKit, - args: Record | undefined, -) => Promise; +export type McpRequestContext = { + client: ImageKit; + stainlessApiKey?: string | undefined; +}; + +export type HandlerFunction = ({ + reqContext, + args, +}: { + reqContext: McpRequestContext; + args: Record | undefined; +}) => Promise; export function asTextContentResult(result: unknown): ToolCallResult { return { From 7738ab86d47fbca9a3c05c2cd48910d43d557c43 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 18 Feb 2026 19:32:35 +0000 Subject: [PATCH 17/50] chore(internal): cache fetch instruction calls in MCP server --- packages/mcp-server/src/instructions.ts | 74 +++++++++++++++++++++++++ packages/mcp-server/src/server.ts | 38 +------------ 2 files changed, 75 insertions(+), 37 deletions(-) create mode 100644 packages/mcp-server/src/instructions.ts diff --git a/packages/mcp-server/src/instructions.ts b/packages/mcp-server/src/instructions.ts new file mode 100644 index 00000000..9b2b945a --- /dev/null +++ b/packages/mcp-server/src/instructions.ts @@ -0,0 +1,74 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { readEnv } from './util'; + +const INSTRUCTIONS_CACHE_TTL_MS = 15 * 60 * 1000; // 15 minutes + +interface InstructionsCacheEntry { + fetchedInstructions: string; + fetchedAt: number; +} + +const instructionsCache = new Map(); + +// Periodically evict stale entries so the cache doesn't grow unboundedly. +const _cacheCleanupInterval = setInterval(() => { + const now = Date.now(); + for (const [key, entry] of instructionsCache) { + if (now - entry.fetchedAt > INSTRUCTIONS_CACHE_TTL_MS) { + instructionsCache.delete(key); + } + } +}, INSTRUCTIONS_CACHE_TTL_MS); + +// Don't keep the process alive just for cleanup. +_cacheCleanupInterval.unref(); + +export async function getInstructions(stainlessApiKey: string | undefined): Promise { + const cacheKey = stainlessApiKey ?? ''; + const cached = instructionsCache.get(cacheKey); + + if (cached && Date.now() - cached.fetchedAt <= INSTRUCTIONS_CACHE_TTL_MS) { + return cached.fetchedInstructions; + } + + const fetchedInstructions = await fetchLatestInstructions(stainlessApiKey); + instructionsCache.set(cacheKey, { fetchedInstructions, fetchedAt: Date.now() }); + return fetchedInstructions; +} + +async function fetchLatestInstructions(stainlessApiKey: string | undefined): Promise { + // Setting the stainless API key is optional, but may be required + // to authenticate requests to the Stainless API. + const response = await fetch( + readEnv('CODE_MODE_INSTRUCTIONS_URL') ?? 'https://api.stainless.com/api/ai/instructions/imagekit', + { + method: 'GET', + headers: { ...(stainlessApiKey && { Authorization: stainlessApiKey }) }, + }, + ); + + let instructions: string | undefined; + if (!response.ok) { + console.warn( + 'Warning: failed to retrieve MCP server instructions. Proceeding with default instructions...', + ); + + instructions = ` + This is the imagekit MCP server. You will use Code Mode to help the user perform + actions. You can use search_docs tool to learn about how to take action with this server. Then, + you will write TypeScript code using the execute tool take action. It is CRITICAL that you be + thoughtful and deliberate when executing code. Always try to entirely solve the problem in code + block: it can be as long as you need to get the job done! + `; + } + + instructions ??= ((await response.json()) as { instructions: string }).instructions; + instructions = ` + If needed, you can get the current time by executing Date.now(). + + ${instructions} + `; + + return instructions; +} diff --git a/packages/mcp-server/src/server.ts b/packages/mcp-server/src/server.ts index c1600288..3e53993d 100644 --- a/packages/mcp-server/src/server.ts +++ b/packages/mcp-server/src/server.ts @@ -11,46 +11,10 @@ import { ClientOptions } from '@imagekit/nodejs'; import ImageKit from '@imagekit/nodejs'; import { codeTool } from './code-tool'; import docsSearchTool from './docs-search-tool'; +import { getInstructions } from './instructions'; import { McpOptions } from './options'; import { blockedMethodsForCodeTool } from './methods'; import { HandlerFunction, McpRequestContext, ToolCallResult, McpTool } from './types'; -import { readEnv } from './util'; - -async function getInstructions(stainlessApiKey: string | undefined): Promise { - // Setting the stainless API key is optional, but may be required - // to authenticate requests to the Stainless API. - const response = await fetch( - readEnv('CODE_MODE_INSTRUCTIONS_URL') ?? 'https://api.stainless.com/api/ai/instructions/imagekit', - { - method: 'GET', - headers: { ...(stainlessApiKey && { Authorization: stainlessApiKey }) }, - }, - ); - - let instructions: string | undefined; - if (!response.ok) { - console.warn( - 'Warning: failed to retrieve MCP server instructions. Proceeding with default instructions...', - ); - - instructions = ` - This is the imagekit MCP server. You will use Code Mode to help the user perform - actions. You can use search_docs tool to learn about how to take action with this server. Then, - you will write TypeScript code using the execute tool take action. It is CRITICAL that you be - thoughtful and deliberate when executing code. Always try to entirely solve the problem in code - block: it can be as long as you need to get the job done! - `; - } - - instructions ??= ((await response.json()) as { instructions: string }).instructions; - instructions = ` - The current time in Unix timestamps is ${Date.now()}. - - ${instructions} - `; - - return instructions; -} export const newMcpServer = async (stainlessApiKey: string | undefined) => new McpServer( From 66c53054ce63794350a9939b8f38ecea3ddec428 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 19 Feb 2026 12:33:08 +0000 Subject: [PATCH 18/50] fix(mcp): initialize SDK lazily to avoid failing the connection on init errors --- packages/mcp-server/src/http.ts | 31 +++++---------- packages/mcp-server/src/index.ts | 2 +- packages/mcp-server/src/server.ts | 64 ++++++++++++++++++++++++------- 3 files changed, 62 insertions(+), 35 deletions(-) diff --git a/packages/mcp-server/src/http.ts b/packages/mcp-server/src/http.ts index 4632176b..f3354511 100644 --- a/packages/mcp-server/src/http.ts +++ b/packages/mcp-server/src/http.ts @@ -24,28 +24,17 @@ const newServer = async ({ const stainlessApiKey = getStainlessApiKey(req, mcpOptions); const server = await newMcpServer(stainlessApiKey); - try { - const authOptions = parseClientAuthHeaders(req, false); + const authOptions = parseClientAuthHeaders(req, false); - await initMcpServer({ - server: server, - mcpOptions: mcpOptions, - clientOptions: { - ...clientOptions, - ...authOptions, - }, - stainlessApiKey: stainlessApiKey, - }); - } catch (error) { - res.status(401).json({ - jsonrpc: '2.0', - error: { - code: -32000, - message: `Unauthorized: ${error instanceof Error ? error.message : error}`, - }, - }); - return null; - } + await initMcpServer({ + server: server, + mcpOptions: mcpOptions, + clientOptions: { + ...clientOptions, + ...authOptions, + }, + stainlessApiKey: stainlessApiKey, + }); return server; }; diff --git a/packages/mcp-server/src/index.ts b/packages/mcp-server/src/index.ts index 003a7655..654d25cf 100644 --- a/packages/mcp-server/src/index.ts +++ b/packages/mcp-server/src/index.ts @@ -24,7 +24,7 @@ async function main() { await launchStreamableHTTPServer({ mcpOptions: options, debug: options.debug, - port: options.port ?? options.socket, + port: options.socket ?? options.port, }); break; } diff --git a/packages/mcp-server/src/server.ts b/packages/mcp-server/src/server.ts index 3e53993d..5930eece 100644 --- a/packages/mcp-server/src/server.ts +++ b/packages/mcp-server/src/server.ts @@ -55,14 +55,32 @@ export async function initMcpServer(params: { error: logAtLevel('error'), }; - let client = new ImageKit({ - logger, - ...params.clientOptions, - defaultHeaders: { - ...params.clientOptions?.defaultHeaders, - 'X-Stainless-MCP': 'true', - }, - }); + let _client: ImageKit | undefined; + let _clientError: Error | undefined; + let _logLevel: 'debug' | 'info' | 'warn' | 'error' | 'off' | undefined; + + const getClient = (): ImageKit => { + if (_clientError) throw _clientError; + if (!_client) { + try { + _client = new ImageKit({ + logger, + ...params.clientOptions, + defaultHeaders: { + ...params.clientOptions?.defaultHeaders, + 'X-Stainless-MCP': 'true', + }, + }); + if (_logLevel) { + _client = _client.withOptions({ logLevel: _logLevel }); + } + } catch (e) { + _clientError = e instanceof Error ? e : new Error(String(e)); + throw _clientError; + } + } + return _client; + }; const providedTools = selectTools(params.mcpOptions); const toolMap = Object.fromEntries(providedTools.map((mcpTool) => [mcpTool.tool.name, mcpTool])); @@ -80,6 +98,21 @@ export async function initMcpServer(params: { throw new Error(`Unknown tool: ${name}`); } + let client: ImageKit; + try { + client = getClient(); + } catch (error) { + return { + content: [ + { + type: 'text' as const, + text: `Failed to initialize client: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + isError: true, + }; + } + return executeHandler({ handler: mcpTool.handler, reqContext: { @@ -92,24 +125,29 @@ export async function initMcpServer(params: { server.setRequestHandler(SetLevelRequestSchema, async (request) => { const { level } = request.params; + let logLevel: 'debug' | 'info' | 'warn' | 'error' | 'off'; switch (level) { case 'debug': - client = client.withOptions({ logLevel: 'debug' }); + logLevel = 'debug'; break; case 'info': - client = client.withOptions({ logLevel: 'info' }); + logLevel = 'info'; break; case 'notice': case 'warning': - client = client.withOptions({ logLevel: 'warn' }); + logLevel = 'warn'; break; case 'error': - client = client.withOptions({ logLevel: 'error' }); + logLevel = 'error'; break; default: - client = client.withOptions({ logLevel: 'off' }); + logLevel = 'off'; break; } + _logLevel = logLevel; + if (_client) { + _client = _client.withOptions({ logLevel }); + } return {}; }); } From f1deef8b69950e5917919a9fc619b558a65fe5b7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 19 Feb 2026 16:03:29 +0000 Subject: [PATCH 19/50] chore(internal): remove mock server code --- scripts/mock | 41 ----------------------------------------- scripts/test | 46 ---------------------------------------------- 2 files changed, 87 deletions(-) delete mode 100755 scripts/mock diff --git a/scripts/mock b/scripts/mock deleted file mode 100755 index 0b28f6ea..00000000 --- a/scripts/mock +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env bash - -set -e - -cd "$(dirname "$0")/.." - -if [[ -n "$1" && "$1" != '--'* ]]; then - URL="$1" - shift -else - URL="$(grep 'openapi_spec_url' .stats.yml | cut -d' ' -f2)" -fi - -# Check if the URL is empty -if [ -z "$URL" ]; then - echo "Error: No OpenAPI spec path/url provided or found in .stats.yml" - exit 1 -fi - -echo "==> Starting mock server with URL ${URL}" - -# Run prism mock on the given spec -if [ "$1" == "--daemon" ]; then - npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" &> .prism.log & - - # Wait for server to come online - echo -n "Waiting for server" - while ! grep -q "✖ fatal\|Prism is listening" ".prism.log" ; do - echo -n "." - sleep 0.1 - done - - if grep -q "✖ fatal" ".prism.log"; then - cat .prism.log - exit 1 - fi - - echo -else - npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" -fi diff --git a/scripts/test b/scripts/test index 7bce0516..548da9bb 100755 --- a/scripts/test +++ b/scripts/test @@ -4,53 +4,7 @@ set -e cd "$(dirname "$0")/.." -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[0;33m' -NC='\033[0m' # No Color -function prism_is_running() { - curl --silent "http://localhost:4010" >/dev/null 2>&1 -} - -kill_server_on_port() { - pids=$(lsof -t -i tcp:"$1" || echo "") - if [ "$pids" != "" ]; then - kill "$pids" - echo "Stopped $pids." - fi -} - -function is_overriding_api_base_url() { - [ -n "$TEST_API_BASE_URL" ] -} - -if ! is_overriding_api_base_url && ! prism_is_running ; then - # When we exit this script, make sure to kill the background mock server process - trap 'kill_server_on_port 4010' EXIT - - # Start the dev server - ./scripts/mock --daemon -fi - -if is_overriding_api_base_url ; then - echo -e "${GREEN}✔ Running tests against ${TEST_API_BASE_URL}${NC}" - echo -elif ! prism_is_running ; then - echo -e "${RED}ERROR:${NC} The test suite will not run without a mock Prism server" - echo -e "running against your OpenAPI spec." - echo - echo -e "To run the server, pass in the path or url of your OpenAPI" - echo -e "spec to the prism command:" - echo - echo -e " \$ ${YELLOW}npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock path/to/your.openapi.yml${NC}" - echo - - exit 1 -else - echo -e "${GREEN}✔ Mock prism server is running with your OpenAPI spec${NC}" - echo -fi echo "==> Running tests" ./node_modules/.bin/jest "$@" From 8d9ad04f864ca543a065a99cc041a436dd53069a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 19 Feb 2026 16:33:48 +0000 Subject: [PATCH 20/50] chore: update mock server docs --- CONTRIBUTING.md | 6 ----- tests/api-resources/accounts/origins.test.ts | 14 ++++++------ .../accounts/url-endpoints.test.ts | 14 ++++++------ tests/api-resources/accounts/usage.test.ts | 4 ++-- tests/api-resources/assets.test.ts | 4 ++-- tests/api-resources/beta/v2/files.test.ts | 4 ++-- .../api-resources/cache/invalidation.test.ts | 6 ++--- .../custom-metadata-fields.test.ts | 14 ++++++------ tests/api-resources/files/bulk.test.ts | 16 +++++++------- tests/api-resources/files/files.test.ts | 22 +++++++++---------- tests/api-resources/files/metadata.test.ts | 6 ++--- tests/api-resources/files/versions.test.ts | 14 ++++++------ tests/api-resources/folders/folders.test.ts | 20 ++++++++--------- tests/api-resources/folders/job.test.ts | 2 +- tests/api-resources/saved-extensions.test.ts | 12 +++++----- 15 files changed, 76 insertions(+), 82 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 61483a34..ebe0537d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -65,12 +65,6 @@ $ pnpm link -—global @imagekit/nodejs ## Running tests -Most tests require you to [set up a mock server](https://github.com/stoplightio/prism) against the OpenAPI spec to run the tests. - -```sh -$ npx prism mock path/to/your/openapi.yml -``` - ```sh $ yarn run test ``` diff --git a/tests/api-resources/accounts/origins.test.ts b/tests/api-resources/accounts/origins.test.ts index f912c7ea..2be57f09 100644 --- a/tests/api-resources/accounts/origins.test.ts +++ b/tests/api-resources/accounts/origins.test.ts @@ -9,7 +9,7 @@ const client = new ImageKit({ }); describe('resource origins', () => { - // Prism tests are disabled + // Mock server tests are disabled test.skip('create: only required params', async () => { const responsePromise = client.accounts.origins.create({ accessKey: 'AKIAIOSFODNN7EXAMPLE', @@ -27,7 +27,7 @@ describe('resource origins', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('create: required and optional params', async () => { const response = await client.accounts.origins.create({ accessKey: 'AKIAIOSFODNN7EXAMPLE', @@ -41,7 +41,7 @@ describe('resource origins', () => { }); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('update: only required params', async () => { const responsePromise = client.accounts.origins.update('id', { accessKey: 'AKIAIOSFODNN7EXAMPLE', @@ -59,7 +59,7 @@ describe('resource origins', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('update: required and optional params', async () => { const response = await client.accounts.origins.update('id', { accessKey: 'AKIAIOSFODNN7EXAMPLE', @@ -73,7 +73,7 @@ describe('resource origins', () => { }); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('list', async () => { const responsePromise = client.accounts.origins.list(); const rawResponse = await responsePromise.asResponse(); @@ -85,7 +85,7 @@ describe('resource origins', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('delete', async () => { const responsePromise = client.accounts.origins.delete('id'); const rawResponse = await responsePromise.asResponse(); @@ -97,7 +97,7 @@ describe('resource origins', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('get', async () => { const responsePromise = client.accounts.origins.get('id'); const rawResponse = await responsePromise.asResponse(); diff --git a/tests/api-resources/accounts/url-endpoints.test.ts b/tests/api-resources/accounts/url-endpoints.test.ts index eea18604..b53af030 100644 --- a/tests/api-resources/accounts/url-endpoints.test.ts +++ b/tests/api-resources/accounts/url-endpoints.test.ts @@ -9,7 +9,7 @@ const client = new ImageKit({ }); describe('resource urlEndpoints', () => { - // Prism tests are disabled + // Mock server tests are disabled test.skip('create: only required params', async () => { const responsePromise = client.accounts.urlEndpoints.create({ description: 'My custom URL endpoint' }); const rawResponse = await responsePromise.asResponse(); @@ -21,7 +21,7 @@ describe('resource urlEndpoints', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('create: required and optional params', async () => { const response = await client.accounts.urlEndpoints.create({ description: 'My custom URL endpoint', @@ -31,7 +31,7 @@ describe('resource urlEndpoints', () => { }); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('update: only required params', async () => { const responsePromise = client.accounts.urlEndpoints.update('id', { description: 'My custom URL endpoint', @@ -45,7 +45,7 @@ describe('resource urlEndpoints', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('update: required and optional params', async () => { const response = await client.accounts.urlEndpoints.update('id', { description: 'My custom URL endpoint', @@ -55,7 +55,7 @@ describe('resource urlEndpoints', () => { }); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('list', async () => { const responsePromise = client.accounts.urlEndpoints.list(); const rawResponse = await responsePromise.asResponse(); @@ -67,7 +67,7 @@ describe('resource urlEndpoints', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('delete', async () => { const responsePromise = client.accounts.urlEndpoints.delete('id'); const rawResponse = await responsePromise.asResponse(); @@ -79,7 +79,7 @@ describe('resource urlEndpoints', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('get', async () => { const responsePromise = client.accounts.urlEndpoints.get('id'); const rawResponse = await responsePromise.asResponse(); diff --git a/tests/api-resources/accounts/usage.test.ts b/tests/api-resources/accounts/usage.test.ts index 98f51e3f..161cdc71 100644 --- a/tests/api-resources/accounts/usage.test.ts +++ b/tests/api-resources/accounts/usage.test.ts @@ -9,7 +9,7 @@ const client = new ImageKit({ }); describe('resource usage', () => { - // Prism tests are disabled + // Mock server tests are disabled test.skip('get: only required params', async () => { const responsePromise = client.accounts.usage.get({ endDate: '2019-12-27', startDate: '2019-12-27' }); const rawResponse = await responsePromise.asResponse(); @@ -21,7 +21,7 @@ describe('resource usage', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('get: required and optional params', async () => { const response = await client.accounts.usage.get({ endDate: '2019-12-27', startDate: '2019-12-27' }); }); diff --git a/tests/api-resources/assets.test.ts b/tests/api-resources/assets.test.ts index 4688d817..bf41276e 100644 --- a/tests/api-resources/assets.test.ts +++ b/tests/api-resources/assets.test.ts @@ -9,7 +9,7 @@ const client = new ImageKit({ }); describe('resource assets', () => { - // Prism tests are disabled + // Mock server tests are disabled test.skip('list', async () => { const responsePromise = client.assets.list(); const rawResponse = await responsePromise.asResponse(); @@ -21,7 +21,7 @@ describe('resource assets', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('list: request options and params are passed correctly', async () => { // ensure the request options are being passed correctly by passing an invalid HTTP method in order to cause an error await expect( diff --git a/tests/api-resources/beta/v2/files.test.ts b/tests/api-resources/beta/v2/files.test.ts index c3fc2bc0..6d1d5b84 100644 --- a/tests/api-resources/beta/v2/files.test.ts +++ b/tests/api-resources/beta/v2/files.test.ts @@ -9,7 +9,7 @@ const client = new ImageKit({ }); describe('resource files', () => { - // Prism tests are disabled + // Mock server tests are disabled test.skip('upload: only required params', async () => { const responsePromise = client.beta.v2.files.upload({ file: await toFile(Buffer.from('# my file contents'), 'README.md'), @@ -24,7 +24,7 @@ describe('resource files', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('upload: required and optional params', async () => { const response = await client.beta.v2.files.upload({ file: await toFile(Buffer.from('# my file contents'), 'README.md'), diff --git a/tests/api-resources/cache/invalidation.test.ts b/tests/api-resources/cache/invalidation.test.ts index 02f3d4cc..d804f743 100644 --- a/tests/api-resources/cache/invalidation.test.ts +++ b/tests/api-resources/cache/invalidation.test.ts @@ -9,7 +9,7 @@ const client = new ImageKit({ }); describe('resource invalidation', () => { - // Prism tests are disabled + // Mock server tests are disabled test.skip('create: only required params', async () => { const responsePromise = client.cache.invalidation.create({ url: 'https://ik.imagekit.io/your_imagekit_id/default-image.jpg', @@ -23,14 +23,14 @@ describe('resource invalidation', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('create: required and optional params', async () => { const response = await client.cache.invalidation.create({ url: 'https://ik.imagekit.io/your_imagekit_id/default-image.jpg', }); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('get', async () => { const responsePromise = client.cache.invalidation.get('requestId'); const rawResponse = await responsePromise.asResponse(); diff --git a/tests/api-resources/custom-metadata-fields.test.ts b/tests/api-resources/custom-metadata-fields.test.ts index 5f36ce66..3fbf78f7 100644 --- a/tests/api-resources/custom-metadata-fields.test.ts +++ b/tests/api-resources/custom-metadata-fields.test.ts @@ -9,7 +9,7 @@ const client = new ImageKit({ }); describe('resource customMetadataFields', () => { - // Prism tests are disabled + // Mock server tests are disabled test.skip('create: only required params', async () => { const responsePromise = client.customMetadataFields.create({ label: 'price', @@ -25,7 +25,7 @@ describe('resource customMetadataFields', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('create: required and optional params', async () => { const response = await client.customMetadataFields.create({ label: 'price', @@ -43,7 +43,7 @@ describe('resource customMetadataFields', () => { }); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('update', async () => { const responsePromise = client.customMetadataFields.update('id'); const rawResponse = await responsePromise.asResponse(); @@ -55,7 +55,7 @@ describe('resource customMetadataFields', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('update: request options and params are passed correctly', async () => { // ensure the request options are being passed correctly by passing an invalid HTTP method in order to cause an error await expect( @@ -78,7 +78,7 @@ describe('resource customMetadataFields', () => { ).rejects.toThrow(ImageKit.NotFoundError); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('list', async () => { const responsePromise = client.customMetadataFields.list(); const rawResponse = await responsePromise.asResponse(); @@ -90,7 +90,7 @@ describe('resource customMetadataFields', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('list: request options and params are passed correctly', async () => { // ensure the request options are being passed correctly by passing an invalid HTTP method in order to cause an error await expect( @@ -101,7 +101,7 @@ describe('resource customMetadataFields', () => { ).rejects.toThrow(ImageKit.NotFoundError); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('delete', async () => { const responsePromise = client.customMetadataFields.delete('id'); const rawResponse = await responsePromise.asResponse(); diff --git a/tests/api-resources/files/bulk.test.ts b/tests/api-resources/files/bulk.test.ts index c5c290a7..1c417b90 100644 --- a/tests/api-resources/files/bulk.test.ts +++ b/tests/api-resources/files/bulk.test.ts @@ -9,7 +9,7 @@ const client = new ImageKit({ }); describe('resource bulk', () => { - // Prism tests are disabled + // Mock server tests are disabled test.skip('delete: only required params', async () => { const responsePromise = client.files.bulk.delete({ fileIds: ['598821f949c0a938d57563bd', '598821f949c0a938d57563be'], @@ -23,14 +23,14 @@ describe('resource bulk', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('delete: required and optional params', async () => { const response = await client.files.bulk.delete({ fileIds: ['598821f949c0a938d57563bd', '598821f949c0a938d57563be'], }); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('addTags: only required params', async () => { const responsePromise = client.files.bulk.addTags({ fileIds: ['598821f949c0a938d57563bd', '598821f949c0a938d57563be'], @@ -45,7 +45,7 @@ describe('resource bulk', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('addTags: required and optional params', async () => { const response = await client.files.bulk.addTags({ fileIds: ['598821f949c0a938d57563bd', '598821f949c0a938d57563be'], @@ -53,7 +53,7 @@ describe('resource bulk', () => { }); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('removeAITags: only required params', async () => { const responsePromise = client.files.bulk.removeAITags({ AITags: ['t-shirt', 'round-neck', 'sale2019'], @@ -68,7 +68,7 @@ describe('resource bulk', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('removeAITags: required and optional params', async () => { const response = await client.files.bulk.removeAITags({ AITags: ['t-shirt', 'round-neck', 'sale2019'], @@ -76,7 +76,7 @@ describe('resource bulk', () => { }); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('removeTags: only required params', async () => { const responsePromise = client.files.bulk.removeTags({ fileIds: ['598821f949c0a938d57563bd', '598821f949c0a938d57563be'], @@ -91,7 +91,7 @@ describe('resource bulk', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('removeTags: required and optional params', async () => { const response = await client.files.bulk.removeTags({ fileIds: ['598821f949c0a938d57563bd', '598821f949c0a938d57563be'], diff --git a/tests/api-resources/files/files.test.ts b/tests/api-resources/files/files.test.ts index c744c6f6..ff75998c 100644 --- a/tests/api-resources/files/files.test.ts +++ b/tests/api-resources/files/files.test.ts @@ -9,7 +9,7 @@ const client = new ImageKit({ }); describe('resource files', () => { - // Prism tests are disabled + // Mock server tests are disabled test.skip('update', async () => { const responsePromise = client.files.update('fileId', {}); const rawResponse = await responsePromise.asResponse(); @@ -21,7 +21,7 @@ describe('resource files', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('delete', async () => { const responsePromise = client.files.delete('fileId'); const rawResponse = await responsePromise.asResponse(); @@ -33,7 +33,7 @@ describe('resource files', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('copy: only required params', async () => { const responsePromise = client.files.copy({ destinationPath: '/folder/to/copy/into/', @@ -48,7 +48,7 @@ describe('resource files', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('copy: required and optional params', async () => { const response = await client.files.copy({ destinationPath: '/folder/to/copy/into/', @@ -57,7 +57,7 @@ describe('resource files', () => { }); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('get', async () => { const responsePromise = client.files.get('fileId'); const rawResponse = await responsePromise.asResponse(); @@ -69,7 +69,7 @@ describe('resource files', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('move: only required params', async () => { const responsePromise = client.files.move({ destinationPath: '/folder/to/move/into/', @@ -84,7 +84,7 @@ describe('resource files', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('move: required and optional params', async () => { const response = await client.files.move({ destinationPath: '/folder/to/move/into/', @@ -92,7 +92,7 @@ describe('resource files', () => { }); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('rename: only required params', async () => { const responsePromise = client.files.rename({ filePath: '/path/to/file.jpg', @@ -107,7 +107,7 @@ describe('resource files', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('rename: required and optional params', async () => { const response = await client.files.rename({ filePath: '/path/to/file.jpg', @@ -116,7 +116,7 @@ describe('resource files', () => { }); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('upload: only required params', async () => { const responsePromise = client.files.upload({ file: await toFile(Buffer.from('# my file contents'), 'README.md'), @@ -131,7 +131,7 @@ describe('resource files', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('upload: required and optional params', async () => { const response = await client.files.upload({ file: await toFile(Buffer.from('# my file contents'), 'README.md'), diff --git a/tests/api-resources/files/metadata.test.ts b/tests/api-resources/files/metadata.test.ts index b7da8ebf..fd318072 100644 --- a/tests/api-resources/files/metadata.test.ts +++ b/tests/api-resources/files/metadata.test.ts @@ -9,7 +9,7 @@ const client = new ImageKit({ }); describe('resource metadata', () => { - // Prism tests are disabled + // Mock server tests are disabled test.skip('get', async () => { const responsePromise = client.files.metadata.get('fileId'); const rawResponse = await responsePromise.asResponse(); @@ -21,7 +21,7 @@ describe('resource metadata', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('getFromURL: only required params', async () => { const responsePromise = client.files.metadata.getFromURL({ url: 'https://example.com' }); const rawResponse = await responsePromise.asResponse(); @@ -33,7 +33,7 @@ describe('resource metadata', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('getFromURL: required and optional params', async () => { const response = await client.files.metadata.getFromURL({ url: 'https://example.com' }); }); diff --git a/tests/api-resources/files/versions.test.ts b/tests/api-resources/files/versions.test.ts index c1bb6d4c..873ec8cc 100644 --- a/tests/api-resources/files/versions.test.ts +++ b/tests/api-resources/files/versions.test.ts @@ -9,7 +9,7 @@ const client = new ImageKit({ }); describe('resource versions', () => { - // Prism tests are disabled + // Mock server tests are disabled test.skip('list', async () => { const responsePromise = client.files.versions.list('fileId'); const rawResponse = await responsePromise.asResponse(); @@ -21,7 +21,7 @@ describe('resource versions', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('delete: only required params', async () => { const responsePromise = client.files.versions.delete('versionId', { fileId: 'fileId' }); const rawResponse = await responsePromise.asResponse(); @@ -33,12 +33,12 @@ describe('resource versions', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('delete: required and optional params', async () => { const response = await client.files.versions.delete('versionId', { fileId: 'fileId' }); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('get: only required params', async () => { const responsePromise = client.files.versions.get('versionId', { fileId: 'fileId' }); const rawResponse = await responsePromise.asResponse(); @@ -50,12 +50,12 @@ describe('resource versions', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('get: required and optional params', async () => { const response = await client.files.versions.get('versionId', { fileId: 'fileId' }); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('restore: only required params', async () => { const responsePromise = client.files.versions.restore('versionId', { fileId: 'fileId' }); const rawResponse = await responsePromise.asResponse(); @@ -67,7 +67,7 @@ describe('resource versions', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('restore: required and optional params', async () => { const response = await client.files.versions.restore('versionId', { fileId: 'fileId' }); }); diff --git a/tests/api-resources/folders/folders.test.ts b/tests/api-resources/folders/folders.test.ts index a983ec4a..c4672ba4 100644 --- a/tests/api-resources/folders/folders.test.ts +++ b/tests/api-resources/folders/folders.test.ts @@ -9,7 +9,7 @@ const client = new ImageKit({ }); describe('resource folders', () => { - // Prism tests are disabled + // Mock server tests are disabled test.skip('create: only required params', async () => { const responsePromise = client.folders.create({ folderName: 'summer', @@ -24,7 +24,7 @@ describe('resource folders', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('create: required and optional params', async () => { const response = await client.folders.create({ folderName: 'summer', @@ -32,7 +32,7 @@ describe('resource folders', () => { }); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('delete: only required params', async () => { const responsePromise = client.folders.delete({ folderPath: '/folder/to/delete/' }); const rawResponse = await responsePromise.asResponse(); @@ -44,12 +44,12 @@ describe('resource folders', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('delete: required and optional params', async () => { const response = await client.folders.delete({ folderPath: '/folder/to/delete/' }); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('copy: only required params', async () => { const responsePromise = client.folders.copy({ destinationPath: '/path/of/destination/folder', @@ -64,7 +64,7 @@ describe('resource folders', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('copy: required and optional params', async () => { const response = await client.folders.copy({ destinationPath: '/path/of/destination/folder', @@ -73,7 +73,7 @@ describe('resource folders', () => { }); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('move: only required params', async () => { const responsePromise = client.folders.move({ destinationPath: '/path/of/destination/folder', @@ -88,7 +88,7 @@ describe('resource folders', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('move: required and optional params', async () => { const response = await client.folders.move({ destinationPath: '/path/of/destination/folder', @@ -96,7 +96,7 @@ describe('resource folders', () => { }); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('rename: only required params', async () => { const responsePromise = client.folders.rename({ folderPath: '/path/of/folder', @@ -111,7 +111,7 @@ describe('resource folders', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('rename: required and optional params', async () => { const response = await client.folders.rename({ folderPath: '/path/of/folder', diff --git a/tests/api-resources/folders/job.test.ts b/tests/api-resources/folders/job.test.ts index e15071f7..7ab1e5ac 100644 --- a/tests/api-resources/folders/job.test.ts +++ b/tests/api-resources/folders/job.test.ts @@ -9,7 +9,7 @@ const client = new ImageKit({ }); describe('resource job', () => { - // Prism tests are disabled + // Mock server tests are disabled test.skip('get', async () => { const responsePromise = client.folders.job.get('jobId'); const rawResponse = await responsePromise.asResponse(); diff --git a/tests/api-resources/saved-extensions.test.ts b/tests/api-resources/saved-extensions.test.ts index 07d9d782..5b4a731e 100644 --- a/tests/api-resources/saved-extensions.test.ts +++ b/tests/api-resources/saved-extensions.test.ts @@ -9,7 +9,7 @@ const client = new ImageKit({ }); describe('resource savedExtensions', () => { - // Prism tests are disabled + // Mock server tests are disabled test.skip('create: only required params', async () => { const responsePromise = client.savedExtensions.create({ config: { name: 'remove-bg' }, @@ -25,7 +25,7 @@ describe('resource savedExtensions', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('create: required and optional params', async () => { const response = await client.savedExtensions.create({ config: { @@ -42,7 +42,7 @@ describe('resource savedExtensions', () => { }); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('update', async () => { const responsePromise = client.savedExtensions.update('id', {}); const rawResponse = await responsePromise.asResponse(); @@ -54,7 +54,7 @@ describe('resource savedExtensions', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('list', async () => { const responsePromise = client.savedExtensions.list(); const rawResponse = await responsePromise.asResponse(); @@ -66,7 +66,7 @@ describe('resource savedExtensions', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('delete', async () => { const responsePromise = client.savedExtensions.delete('id'); const rawResponse = await responsePromise.asResponse(); @@ -78,7 +78,7 @@ describe('resource savedExtensions', () => { expect(dataAndResponse.response).toBe(rawResponse); }); - // Prism tests are disabled + // Mock server tests are disabled test.skip('get', async () => { const responsePromise = client.savedExtensions.get('id'); const rawResponse = await responsePromise.asResponse(); From 57fa966d57b1069c554658921a3d649a563e4e12 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 19 Feb 2026 22:13:23 +0000 Subject: [PATCH 21/50] chore(mcp): correctly update version in sync with sdk --- release-please-config.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/release-please-config.json b/release-please-config.json index b1909804..9b042792 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -68,6 +68,11 @@ "type": "json", "path": "packages/mcp-server/package.json", "jsonpath": "$.version" + }, + { + "type": "json", + "path": "packages/mcp-server/manifest.json", + "jsonpath": "$.version" } ] } From e39e3777d9d0ccd225774c2a889aa6b6c81a6402 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 07:23:06 +0000 Subject: [PATCH 22/50] chore(internal): update agents version --- packages/mcp-server/cloudflare-worker/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mcp-server/cloudflare-worker/package.json b/packages/mcp-server/cloudflare-worker/package.json index 468245d4..437a04b5 100644 --- a/packages/mcp-server/cloudflare-worker/package.json +++ b/packages/mcp-server/cloudflare-worker/package.json @@ -19,7 +19,7 @@ "dependencies": { "@cloudflare/workers-oauth-provider": "^0.0.5", "@modelcontextprotocol/sdk": "^1.25.2", - "agents": "^0.0.88", + "agents": "^0.0.113", "hono": "^4.11.7", "@imagekit/api-mcp": "latest", "zod": "^3.24.4" From 743a1126b13d90832ccac545474eda7a1094043f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 16:33:35 +0000 Subject: [PATCH 23/50] fix(docs/contributing): correct pnpm link command --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ebe0537d..8cdc92ac 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -60,7 +60,7 @@ $ yarn link @imagekit/nodejs # With pnpm $ pnpm link --global $ cd ../my-package -$ pnpm link -—global @imagekit/nodejs +$ pnpm link --global @imagekit/nodejs ``` ## Running tests From 446fc85863574d74d94918ee09aff23f4ed373b4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 23:08:35 +0000 Subject: [PATCH 24/50] chore(internal): upgrade @modelcontextprotocol/sdk and hono --- packages/mcp-server/cloudflare-worker/package.json | 4 ++-- packages/mcp-server/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/mcp-server/cloudflare-worker/package.json b/packages/mcp-server/cloudflare-worker/package.json index 437a04b5..cb5597e9 100644 --- a/packages/mcp-server/cloudflare-worker/package.json +++ b/packages/mcp-server/cloudflare-worker/package.json @@ -18,9 +18,9 @@ }, "dependencies": { "@cloudflare/workers-oauth-provider": "^0.0.5", - "@modelcontextprotocol/sdk": "^1.25.2", + "@modelcontextprotocol/sdk": "^1.26.0", "agents": "^0.0.113", - "hono": "^4.11.7", + "hono": "^4.12.1", "@imagekit/api-mcp": "latest", "zod": "^3.24.4" } diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json index b289c973..cffedd65 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -32,7 +32,7 @@ "dependencies": { "@imagekit/nodejs": "file:../../dist/", "@cloudflare/cabidela": "^0.2.4", - "@modelcontextprotocol/sdk": "^1.25.2", + "@modelcontextprotocol/sdk": "^1.26.0", "@valtown/deno-http-worker": "^0.0.21", "cookie-parser": "^1.4.6", "cors": "^2.8.5", From 41789006a0bdd72bf302bea9c493efcca5927f5d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Feb 2026 16:38:10 +0000 Subject: [PATCH 25/50] chore(internal): codegen related update --- packages/mcp-server/cloudflare-worker/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mcp-server/cloudflare-worker/package.json b/packages/mcp-server/cloudflare-worker/package.json index cb5597e9..8f711420 100644 --- a/packages/mcp-server/cloudflare-worker/package.json +++ b/packages/mcp-server/cloudflare-worker/package.json @@ -20,7 +20,7 @@ "@cloudflare/workers-oauth-provider": "^0.0.5", "@modelcontextprotocol/sdk": "^1.26.0", "agents": "^0.0.113", - "hono": "^4.12.1", + "hono": "^4.12.2", "@imagekit/api-mcp": "latest", "zod": "^3.24.4" } From 497c9269a3438a872c1ddb7a213d34abe90d6f6b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Feb 2026 20:04:49 +0000 Subject: [PATCH 26/50] chore(internal): make MCP code execution location configurable via a flag --- packages/mcp-server/Dockerfile | 14 +- packages/mcp-server/src/code-tool-paths.cts | 3 + packages/mcp-server/src/code-tool-types.ts | 1 + packages/mcp-server/src/code-tool-worker.ts | 319 +++++++++++++++++++ packages/mcp-server/src/code-tool.ts | 320 +++++++++++++++++--- packages/mcp-server/src/options.ts | 12 + packages/mcp-server/src/server.ts | 1 + packages/mcp-server/tests/options.test.ts | 20 +- 8 files changed, 620 insertions(+), 70 deletions(-) create mode 100644 packages/mcp-server/src/code-tool-paths.cts create mode 100644 packages/mcp-server/src/code-tool-worker.ts diff --git a/packages/mcp-server/Dockerfile b/packages/mcp-server/Dockerfile index 3fe3e5a4..534294c1 100644 --- a/packages/mcp-server/Dockerfile +++ b/packages/mcp-server/Dockerfile @@ -37,9 +37,21 @@ COPY . . RUN yarn install --frozen-lockfile && \ yarn build -# Production stage +FROM denoland/deno:bin-2.6.10 AS deno_installer +FROM gcr.io/distroless/cc@sha256:66d87e170bc2c5e2b8cf853501141c3c55b4e502b8677595c57534df54a68cc5 AS cc + FROM node:24-alpine +# Install deno +COPY --from=deno_installer /deno /usr/local/bin/deno + +# Add in shared libraries needed by Deno +COPY --from=cc --chown=root:root --chmod=755 /lib/*-linux-gnu/* /usr/local/lib/ +COPY --from=cc --chown=root:root --chmod=755 /lib/ld-linux-* /lib/ + +RUN mkdir /lib64 && ln -s /usr/local/lib/ld-linux-* /lib64/ +ENV LD_LIBRARY_PATH=/usr/lib:/usr/local/lib + # Add non-root user RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001 diff --git a/packages/mcp-server/src/code-tool-paths.cts b/packages/mcp-server/src/code-tool-paths.cts new file mode 100644 index 00000000..15ce7f55 --- /dev/null +++ b/packages/mcp-server/src/code-tool-paths.cts @@ -0,0 +1,3 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +export const workerPath = require.resolve('./code-tool-worker.mjs'); diff --git a/packages/mcp-server/src/code-tool-types.ts b/packages/mcp-server/src/code-tool-types.ts index 394bec98..c29cbd38 100644 --- a/packages/mcp-server/src/code-tool-types.ts +++ b/packages/mcp-server/src/code-tool-types.ts @@ -8,6 +8,7 @@ export type WorkerInput = { client_opts: ClientOptions; intent?: string | undefined; }; + export type WorkerOutput = { is_error: boolean; result: unknown | null; diff --git a/packages/mcp-server/src/code-tool-worker.ts b/packages/mcp-server/src/code-tool-worker.ts new file mode 100644 index 00000000..96bf1a72 --- /dev/null +++ b/packages/mcp-server/src/code-tool-worker.ts @@ -0,0 +1,319 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import path from 'node:path'; +import util from 'node:util'; +import Fuse from 'fuse.js'; +import ts from 'typescript'; +import { WorkerOutput } from './code-tool-types'; +import { ImageKit, ClientOptions } from '@imagekit/nodejs'; + +function getRunFunctionSource(code: string): { + type: 'declaration' | 'expression'; + client: string | undefined; + code: string; +} | null { + const sourceFile = ts.createSourceFile('code.ts', code, ts.ScriptTarget.Latest, true); + const printer = ts.createPrinter(); + + for (const statement of sourceFile.statements) { + // Check for top-level function declarations + if (ts.isFunctionDeclaration(statement)) { + if (statement.name?.text === 'run') { + return { + type: 'declaration', + client: statement.parameters[0]?.name.getText(), + code: printer.printNode(ts.EmitHint.Unspecified, statement.body!, sourceFile), + }; + } + } + + // Check for variable declarations: const run = () => {} or const run = function() {} + if (ts.isVariableStatement(statement)) { + for (const declaration of statement.declarationList.declarations) { + if ( + ts.isIdentifier(declaration.name) && + declaration.name.text === 'run' && + // Check if it's initialized with a function + declaration.initializer && + (ts.isFunctionExpression(declaration.initializer) || ts.isArrowFunction(declaration.initializer)) + ) { + return { + type: 'expression', + client: declaration.initializer.parameters[0]?.name.getText(), + code: printer.printNode(ts.EmitHint.Unspecified, declaration.initializer, sourceFile), + }; + } + } + } + } + + return null; +} + +function getTSDiagnostics(code: string): string[] { + const functionSource = getRunFunctionSource(code)!; + const codeWithImport = [ + 'import { ImageKit } from "@imagekit/nodejs";', + functionSource.type === 'declaration' ? + `async function run(${functionSource.client}: ImageKit)` + : `const run: (${functionSource.client}: ImageKit) => Promise =`, + functionSource.code, + ].join('\n'); + const sourcePath = path.resolve('code.ts'); + const ast = ts.createSourceFile(sourcePath, codeWithImport, ts.ScriptTarget.Latest, true); + const options = ts.getDefaultCompilerOptions(); + options.target = ts.ScriptTarget.Latest; + options.module = ts.ModuleKind.NodeNext; + options.moduleResolution = ts.ModuleResolutionKind.NodeNext; + const host = ts.createCompilerHost(options, true); + const newHost: typeof host = { + ...host, + getSourceFile: (...args) => { + if (path.resolve(args[0]) === sourcePath) { + return ast; + } + return host.getSourceFile(...args); + }, + readFile: (...args) => { + if (path.resolve(args[0]) === sourcePath) { + return codeWithImport; + } + return host.readFile(...args); + }, + fileExists: (...args) => { + if (path.resolve(args[0]) === sourcePath) { + return true; + } + return host.fileExists(...args); + }, + }; + const program = ts.createProgram({ + options, + rootNames: [sourcePath], + host: newHost, + }); + const diagnostics = ts.getPreEmitDiagnostics(program, ast); + return diagnostics.map((d) => { + const message = ts.flattenDiagnosticMessageText(d.messageText, '\n'); + if (!d.file || !d.start) return `- ${message}`; + const { line: lineNumber } = ts.getLineAndCharacterOfPosition(d.file, d.start); + const line = codeWithImport.split('\n').at(lineNumber)?.trim(); + return line ? `- ${message}\n ${line}` : `- ${message}`; + }); +} + +const fuse = new Fuse( + [ + 'client.customMetadataFields.create', + 'client.customMetadataFields.delete', + 'client.customMetadataFields.list', + 'client.customMetadataFields.update', + 'client.files.copy', + 'client.files.delete', + 'client.files.get', + 'client.files.move', + 'client.files.rename', + 'client.files.update', + 'client.files.upload', + 'client.files.bulk.addTags', + 'client.files.bulk.delete', + 'client.files.bulk.removeAITags', + 'client.files.bulk.removeTags', + 'client.files.versions.delete', + 'client.files.versions.get', + 'client.files.versions.list', + 'client.files.versions.restore', + 'client.files.metadata.get', + 'client.files.metadata.getFromURL', + 'client.savedExtensions.create', + 'client.savedExtensions.delete', + 'client.savedExtensions.get', + 'client.savedExtensions.list', + 'client.savedExtensions.update', + 'client.assets.list', + 'client.cache.invalidation.create', + 'client.cache.invalidation.get', + 'client.folders.copy', + 'client.folders.create', + 'client.folders.delete', + 'client.folders.move', + 'client.folders.rename', + 'client.folders.job.get', + 'client.accounts.usage.get', + 'client.accounts.origins.create', + 'client.accounts.origins.delete', + 'client.accounts.origins.get', + 'client.accounts.origins.list', + 'client.accounts.origins.update', + 'client.accounts.urlEndpoints.create', + 'client.accounts.urlEndpoints.delete', + 'client.accounts.urlEndpoints.get', + 'client.accounts.urlEndpoints.list', + 'client.accounts.urlEndpoints.update', + 'client.beta.v2.files.upload', + 'client.webhooks.unsafeUnwrap', + 'client.webhooks.unwrap', + ], + { threshold: 1, shouldSort: true }, +); + +function getMethodSuggestions(fullyQualifiedMethodName: string): string[] { + return fuse + .search(fullyQualifiedMethodName) + .map(({ item }) => item) + .slice(0, 5); +} + +const proxyToObj = new WeakMap(); +const objToProxy = new WeakMap(); + +type ClientProxyConfig = { + path: string[]; + isBelievedBad?: boolean; +}; + +function makeSdkProxy(obj: T, { path, isBelievedBad = false }: ClientProxyConfig): T { + let proxy: T = objToProxy.get(obj); + + if (!proxy) { + proxy = new Proxy(obj, { + get(target, prop, receiver) { + const propPath = [...path, String(prop)]; + const value = Reflect.get(target, prop, receiver); + + if (isBelievedBad || (!(prop in target) && value === undefined)) { + // If we're accessing a path that doesn't exist, it will probably eventually error. + // Let's proxy it and mark it bad so that we can control the error message. + // We proxy an empty class so that an invocation or construction attempt is possible. + return makeSdkProxy(class {}, { path: propPath, isBelievedBad: true }); + } + + if (value !== null && (typeof value === 'object' || typeof value === 'function')) { + return makeSdkProxy(value, { path: propPath, isBelievedBad }); + } + + return value; + }, + + apply(target, thisArg, args) { + if (isBelievedBad || typeof target !== 'function') { + const fullyQualifiedMethodName = path.join('.'); + const suggestions = getMethodSuggestions(fullyQualifiedMethodName); + throw new Error( + `${fullyQualifiedMethodName} is not a function. Did you mean: ${suggestions.join(', ')}`, + ); + } + + return Reflect.apply(target, proxyToObj.get(thisArg) ?? thisArg, args); + }, + + construct(target, args, newTarget) { + if (isBelievedBad || typeof target !== 'function') { + const fullyQualifiedMethodName = path.join('.'); + const suggestions = getMethodSuggestions(fullyQualifiedMethodName); + throw new Error( + `${fullyQualifiedMethodName} is not a constructor. Did you mean: ${suggestions.join(', ')}`, + ); + } + + return Reflect.construct(target, args, newTarget); + }, + }); + + objToProxy.set(obj, proxy); + proxyToObj.set(proxy, obj); + } + + return proxy; +} + +function parseError(code: string, error: unknown): string | undefined { + if (!(error instanceof Error)) return; + const message = error.name ? `${error.name}: ${error.message}` : error.message; + try { + // Deno uses V8; the first ":LINE:COLUMN" is the top of stack. + const lineNumber = error.stack?.match(/:([0-9]+):[0-9]+/)?.[1]; + // -1 for the zero-based indexing + const line = + lineNumber && + code + .split('\n') + .at(parseInt(lineNumber, 10) - 1) + ?.trim(); + return line ? `${message}\n at line ${lineNumber}\n ${line}` : message; + } catch { + return message; + } +} + +const fetch = async (req: Request): Promise => { + const { opts, code } = (await req.json()) as { opts: ClientOptions; code: string }; + + const runFunctionSource = code ? getRunFunctionSource(code) : null; + if (!runFunctionSource) { + const message = + code ? + 'The code is missing a top-level `run` function.' + : 'The code argument is missing. Provide one containing a top-level `run` function.'; + return Response.json( + { + is_error: true, + result: `${message} Write code within this template:\n\n\`\`\`\nasync function run(client) {\n // Fill this out\n}\n\`\`\``, + log_lines: [], + err_lines: [], + } satisfies WorkerOutput, + { status: 400, statusText: 'Code execution error' }, + ); + } + + const diagnostics = getTSDiagnostics(code); + if (diagnostics.length > 0) { + return Response.json( + { + is_error: true, + result: `The code contains TypeScript diagnostics:\n${diagnostics.join('\n')}`, + log_lines: [], + err_lines: [], + } satisfies WorkerOutput, + { status: 400, statusText: 'Code execution error' }, + ); + } + + const client = new ImageKit({ + ...opts, + }); + + const log_lines: string[] = []; + const err_lines: string[] = []; + const console = { + log: (...args: unknown[]) => { + log_lines.push(util.format(...args)); + }, + error: (...args: unknown[]) => { + err_lines.push(util.format(...args)); + }, + }; + try { + let run_ = async (client: any) => {}; + eval(`${code}\nrun_ = run;`); + const result = await run_(makeSdkProxy(client, { path: ['client'] })); + return Response.json({ + is_error: false, + result, + log_lines, + err_lines, + } satisfies WorkerOutput); + } catch (e) { + return Response.json( + { + is_error: true, + result: parseError(code, e), + log_lines, + err_lines, + } satisfies WorkerOutput, + { status: 400, statusText: 'Code execution error' }, + ); + } +}; + +export default { fetch }; diff --git a/packages/mcp-server/src/code-tool.ts b/packages/mcp-server/src/code-tool.ts index 932583e7..f93ca950 100644 --- a/packages/mcp-server/src/code-tool.ts +++ b/packages/mcp-server/src/code-tool.ts @@ -1,6 +1,12 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +import fs from 'node:fs'; +import path from 'node:path'; +import url from 'node:url'; +import { newDenoHTTPWorker } from '@valtown/deno-http-worker'; +import { workerPath } from './code-tool-paths.cjs'; import { + ContentBlock, McpRequestContext, McpTool, Metadata, @@ -12,6 +18,8 @@ import { Tool } from '@modelcontextprotocol/sdk/types.js'; import { readEnv, requireValue } from './util'; import { WorkerInput, WorkerOutput } from './code-tool-types'; import { SdkMethod } from './methods'; +import { McpCodeExecutionMode } from './options'; +import { ClientOptions } from '@imagekit/nodejs'; const prompt = `Runs JavaScript code to interact with the Image Kit API. @@ -40,9 +48,19 @@ Variables will not persist between calls, so make sure to return or log any data * we expose a single tool that can be used to search for endpoints by name, resource, operation, or tag, and then * a generic endpoint that can be used to invoke any endpoint with the provided arguments. * - * @param endpoints - The endpoints to include in the list. + * @param blockedMethods - The methods to block for code execution. Blocking is done by simple string + * matching, so it is not secure against obfuscation. For stronger security, block in the downstream API + * with limited API keys. + * @param codeExecutionMode - Whether to execute code in a local Deno environment or in a remote + * sandbox environment hosted by Stainless. */ -export function codeTool({ blockedMethods }: { blockedMethods: SdkMethod[] | undefined }): McpTool { +export function codeTool({ + blockedMethods, + codeExecutionMode, +}: { + blockedMethods: SdkMethod[] | undefined; + codeExecutionMode: McpCodeExecutionMode; +}): McpTool { const metadata: Metadata = { resource: 'all', operation: 'write', tags: [] }; const tool: Tool = { name: 'execute', @@ -62,6 +80,7 @@ export function codeTool({ blockedMethods }: { blockedMethods: SdkMethod[] | und required: ['code'], }, }; + const handler = async ({ reqContext, args, @@ -70,9 +89,6 @@ export function codeTool({ blockedMethods }: { blockedMethods: SdkMethod[] | und args: any; }): Promise => { const code = args.code as string; - const intent = args.intent as string | undefined; - const client = reqContext.client; - // Do very basic blocking of code that includes forbidden method names. // // WARNING: This is not secure against obfuscation and other evasion methods. If @@ -89,54 +105,258 @@ export function codeTool({ blockedMethods }: { blockedMethods: SdkMethod[] | und } } - const codeModeEndpoint = - readEnv('CODE_MODE_ENDPOINT_URL') ?? 'https://api.stainless.com/api/ai/code-tool'; - - // Setting a Stainless API key authenticates requests to the code tool endpoint. - const res = await fetch(codeModeEndpoint, { - method: 'POST', - headers: { - ...(reqContext.stainlessApiKey && { Authorization: reqContext.stainlessApiKey }), - 'Content-Type': 'application/json', - client_envs: JSON.stringify({ - IMAGEKIT_PRIVATE_KEY: requireValue( - readEnv('IMAGEKIT_PRIVATE_KEY') ?? client.privateKey, - 'set IMAGEKIT_PRIVATE_KEY environment variable or provide privateKey client option', - ), - OPTIONAL_IMAGEKIT_IGNORES_THIS: - readEnv('OPTIONAL_IMAGEKIT_IGNORES_THIS') ?? client.password ?? undefined, - IMAGEKIT_WEBHOOK_SECRET: readEnv('IMAGEKIT_WEBHOOK_SECRET') ?? client.webhookSecret ?? undefined, - IMAGE_KIT_BASE_URL: readEnv('IMAGE_KIT_BASE_URL') ?? client.baseURL ?? undefined, - }), - }, - body: JSON.stringify({ - project_name: 'imagekit', - code, - intent, - client_opts: {}, - } satisfies WorkerInput), - }); + if (codeExecutionMode === 'local') { + return await localDenoHandler({ reqContext, args }); + } else { + return await remoteStainlessHandler({ reqContext, args }); + } + }; + + return { metadata, tool, handler }; +} + +const remoteStainlessHandler = async ({ + reqContext, + args, +}: { + reqContext: McpRequestContext; + args: any; +}): Promise => { + const code = args.code as string; + const intent = args.intent as string | undefined; + const client = reqContext.client; - if (!res.ok) { - throw new Error( - `${res.status}: ${ - res.statusText - } error when trying to contact Code Tool server. Details: ${await res.text()}`, + const codeModeEndpoint = readEnv('CODE_MODE_ENDPOINT_URL') ?? 'https://api.stainless.com/api/ai/code-tool'; + + // Setting a Stainless API key authenticates requests to the code tool endpoint. + const res = await fetch(codeModeEndpoint, { + method: 'POST', + headers: { + ...(reqContext.stainlessApiKey && { Authorization: reqContext.stainlessApiKey }), + 'Content-Type': 'application/json', + client_envs: JSON.stringify({ + IMAGEKIT_PRIVATE_KEY: requireValue( + readEnv('IMAGEKIT_PRIVATE_KEY') ?? client.privateKey, + 'set IMAGEKIT_PRIVATE_KEY environment variable or provide privateKey client option', + ), + OPTIONAL_IMAGEKIT_IGNORES_THIS: + readEnv('OPTIONAL_IMAGEKIT_IGNORES_THIS') ?? client.password ?? undefined, + IMAGEKIT_WEBHOOK_SECRET: readEnv('IMAGEKIT_WEBHOOK_SECRET') ?? client.webhookSecret ?? undefined, + IMAGE_KIT_BASE_URL: readEnv('IMAGE_KIT_BASE_URL') ?? client.baseURL ?? undefined, + }), + }, + body: JSON.stringify({ + project_name: 'imagekit', + code, + intent, + client_opts: {}, + } satisfies WorkerInput), + }); + + if (!res.ok) { + throw new Error( + `${res.status}: ${ + res.statusText + } error when trying to contact Code Tool server. Details: ${await res.text()}`, + ); + } + + const { is_error, result, log_lines, err_lines } = (await res.json()) as WorkerOutput; + const hasLogs = log_lines.length > 0 || err_lines.length > 0; + const output = { + result, + ...(log_lines.length > 0 && { log_lines }), + ...(err_lines.length > 0 && { err_lines }), + }; + if (is_error) { + return asErrorResult(typeof result === 'string' && !hasLogs ? result : JSON.stringify(output, null, 2)); + } + return asTextContentResult(output); +}; + +const localDenoHandler = async ({ + reqContext, + args, +}: { + reqContext: McpRequestContext; + args: unknown; +}): Promise => { + const client = reqContext.client; + const baseURLHostname = new URL(client.baseURL).hostname; + const { code } = args as { code: string }; + + let denoPath: string; + + const packageRoot = path.resolve(path.dirname(workerPath), '..'); + const packageNodeModulesPath = path.resolve(packageRoot, 'node_modules'); + + // Check if deno is in PATH + const { execSync } = await import('node:child_process'); + try { + execSync('command -v deno', { stdio: 'ignore' }); + denoPath = 'deno'; + } catch { + try { + // Use deno binary in node_modules if it's found + const denoNodeModulesPath = path.resolve(packageNodeModulesPath, 'deno', 'bin.cjs'); + await fs.promises.access(denoNodeModulesPath, fs.constants.X_OK); + denoPath = denoNodeModulesPath; + } catch { + return asErrorResult( + 'Deno is required for code execution but was not found. ' + + 'Install it from https://deno.land or run: npm install deno', ); } + } + + const allowReadPaths = [ + 'code-tool-worker.mjs', + `${workerPath.replace(/([\/\\]node_modules)[\/\\].+$/, '$1')}/`, + packageRoot, + ]; - const { is_error, result, log_lines, err_lines } = (await res.json()) as WorkerOutput; - const hasLogs = log_lines.length > 0 || err_lines.length > 0; - const output = { - result, - ...(log_lines.length > 0 && { log_lines }), - ...(err_lines.length > 0 && { err_lines }), - }; - if (is_error) { - return asErrorResult(typeof result === 'string' && !hasLogs ? result : JSON.stringify(output, null, 2)); + // Follow symlinks in node_modules to allow read access to workspace-linked packages + try { + const sdkPkgName = '@imagekit/nodejs'; + const sdkDir = path.resolve(packageNodeModulesPath, sdkPkgName); + const realSdkDir = fs.realpathSync(sdkDir); + if (realSdkDir !== sdkDir) { + allowReadPaths.push(realSdkDir); } - return asTextContentResult(output); - }; + } catch { + // Ignore if symlink resolution fails + } - return { metadata, tool, handler }; -} + const allowRead = allowReadPaths.join(','); + + const worker = await newDenoHTTPWorker(url.pathToFileURL(workerPath), { + denoExecutable: denoPath, + runFlags: [ + `--node-modules-dir=manual`, + `--allow-read=${allowRead}`, + `--allow-net=${baseURLHostname}`, + // Allow environment variables because instantiating the client will try to read from them, + // even though they are not set. + '--allow-env', + ], + printOutput: true, + spawnOptions: { + cwd: path.dirname(workerPath), + }, + }); + + try { + const resp = await new Promise((resolve, reject) => { + worker.addEventListener('exit', (exitCode) => { + reject(new Error(`Worker exited with code ${exitCode}`)); + }); + + const opts: ClientOptions = { + baseURL: client.baseURL, + privateKey: client.privateKey, + password: client.password, + webhookSecret: client.webhookSecret, + defaultHeaders: { + 'X-Stainless-MCP': 'true', + }, + }; + + const req = worker.request( + 'http://localhost', + { + headers: { + 'content-type': 'application/json', + }, + method: 'POST', + }, + (resp) => { + const body: Uint8Array[] = []; + resp.on('error', (err) => { + reject(err); + }); + resp.on('data', (chunk) => { + body.push(chunk); + }); + resp.on('end', () => { + resolve( + new Response(Buffer.concat(body).toString(), { + status: resp.statusCode ?? 200, + headers: resp.headers as any, + }), + ); + }); + }, + ); + + const body = JSON.stringify({ + opts, + code, + }); + + req.write(body, (err) => { + if (err != null) { + reject(err); + } + }); + + req.end(); + }); + + if (resp.status === 200) { + const { result, log_lines, err_lines } = (await resp.json()) as WorkerOutput; + const returnOutput: ContentBlock | null = + result == null ? null : ( + { + type: 'text', + text: typeof result === 'string' ? result : JSON.stringify(result), + } + ); + const logOutput: ContentBlock | null = + log_lines.length === 0 ? + null + : { + type: 'text', + text: log_lines.join('\n'), + }; + const errOutput: ContentBlock | null = + err_lines.length === 0 ? + null + : { + type: 'text', + text: 'Error output:\n' + err_lines.join('\n'), + }; + return { + content: [returnOutput, logOutput, errOutput].filter((block) => block !== null), + }; + } else { + const { result, log_lines, err_lines } = (await resp.json()) as WorkerOutput; + const messageOutput: ContentBlock | null = + result == null ? null : ( + { + type: 'text', + text: typeof result === 'string' ? result : JSON.stringify(result), + } + ); + const logOutput: ContentBlock | null = + log_lines.length === 0 ? + null + : { + type: 'text', + text: log_lines.join('\n'), + }; + const errOutput: ContentBlock | null = + err_lines.length === 0 ? + null + : { + type: 'text', + text: 'Error output:\n' + err_lines.join('\n'), + }; + return { + content: [messageOutput, logOutput, errOutput].filter((block) => block !== null), + isError: true, + }; + } + } finally { + worker.terminate(); + } +}; diff --git a/packages/mcp-server/src/options.ts b/packages/mcp-server/src/options.ts index 32a88713..9e9d15cd 100644 --- a/packages/mcp-server/src/options.ts +++ b/packages/mcp-server/src/options.ts @@ -19,8 +19,11 @@ export type McpOptions = { codeAllowHttpGets?: boolean | undefined; codeAllowedMethods?: string[] | undefined; codeBlockedMethods?: string[] | undefined; + codeExecutionMode: McpCodeExecutionMode; }; +export type McpCodeExecutionMode = 'stainless-sandbox' | 'local'; + export function parseCLIOptions(): CLIOptions { const opts = yargs(hideBin(process.argv)) .option('code-allow-http-gets', { @@ -40,6 +43,13 @@ export function parseCLIOptions(): CLIOptions { description: 'Methods to explicitly block for code tool. Evaluated as regular expressions against method fully qualified names. If all code-allow-* flags are unset, then everything is allowed.', }) + .option('code-execution-mode', { + type: 'string', + choices: ['stainless-sandbox', 'local'], + default: 'stainless-sandbox', + description: + "Where to run code execution in code tool; 'stainless-sandbox' will execute code in Stainless-hosted sandboxes whereas 'local' will execute code locally on the MCP server machine.", + }) .option('debug', { type: 'boolean', description: 'Enable debug logging' }) .option('no-tools', { type: 'string', @@ -93,6 +103,7 @@ export function parseCLIOptions(): CLIOptions { codeAllowHttpGets: argv.codeAllowHttpGets, codeAllowedMethods: argv.codeAllowedMethods, codeBlockedMethods: argv.codeBlockedMethods, + codeExecutionMode: argv.codeExecutionMode as McpCodeExecutionMode, transport, port: argv.port, socket: argv.socket, @@ -124,6 +135,7 @@ export function parseQueryOptions(defaultOptions: McpOptions, query: unknown): M : defaultOptions.includeDocsTools; return { + codeExecutionMode: defaultOptions.codeExecutionMode, ...(docsTools !== undefined && { includeDocsTools: docsTools }), }; } diff --git a/packages/mcp-server/src/server.ts b/packages/mcp-server/src/server.ts index 5930eece..d17fa460 100644 --- a/packages/mcp-server/src/server.ts +++ b/packages/mcp-server/src/server.ts @@ -159,6 +159,7 @@ export function selectTools(options?: McpOptions): McpTool[] { const includedTools = [ codeTool({ blockedMethods: blockedMethodsForCodeTool(options), + codeExecutionMode: options?.codeExecutionMode ?? 'stainless-sandbox', }), ]; if (options?.includeDocsTools ?? true) { diff --git a/packages/mcp-server/tests/options.test.ts b/packages/mcp-server/tests/options.test.ts index 7a2d5114..17306295 100644 --- a/packages/mcp-server/tests/options.test.ts +++ b/packages/mcp-server/tests/options.test.ts @@ -1,4 +1,4 @@ -import { parseCLIOptions, parseQueryOptions } from '../src/options'; +import { parseCLIOptions } from '../src/options'; // Mock process.argv const mockArgv = (args: string[]) => { @@ -30,21 +30,3 @@ describe('parseCLIOptions', () => { cleanup(); }); }); - -describe('parseQueryOptions', () => { - const defaultOptions = {}; - - it('default parsing should be empty', () => { - const query = ''; - const result = parseQueryOptions(defaultOptions, query); - - expect(result).toEqual({}); - }); - - it('should handle invalid query string gracefully', () => { - const query = 'invalid=value&tools=invalid-operation'; - - // Should throw due to Zod validation for invalid tools - expect(() => parseQueryOptions(defaultOptions, query)).toThrow(); - }); -}); From 62b9ad0590c40c17cfcd1e7a68f2d5b2b6171cd1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 17:03:38 +0000 Subject: [PATCH 27/50] chore(internal): fix MCP Dockerfiles so they can be built without buildkit --- packages/mcp-server/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/mcp-server/Dockerfile b/packages/mcp-server/Dockerfile index 534294c1..2012f0b6 100644 --- a/packages/mcp-server/Dockerfile +++ b/packages/mcp-server/Dockerfile @@ -46,8 +46,8 @@ FROM node:24-alpine COPY --from=deno_installer /deno /usr/local/bin/deno # Add in shared libraries needed by Deno -COPY --from=cc --chown=root:root --chmod=755 /lib/*-linux-gnu/* /usr/local/lib/ -COPY --from=cc --chown=root:root --chmod=755 /lib/ld-linux-* /lib/ +COPY --from=cc /lib/*-linux-gnu/* /usr/local/lib/ +COPY --from=cc /lib/ld-linux-* /lib/ RUN mkdir /lib64 && ln -s /usr/local/lib/ld-linux-* /lib64/ ENV LD_LIBRARY_PATH=/usr/lib:/usr/local/lib From 9dde351b748d8a2e2fe56321b477ae214e679431 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 19:55:32 +0000 Subject: [PATCH 28/50] chore(internal): fix MCP Dockerfiles so they can be built without buildkit --- packages/mcp-server/Dockerfile | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/packages/mcp-server/Dockerfile b/packages/mcp-server/Dockerfile index 2012f0b6..dc521775 100644 --- a/packages/mcp-server/Dockerfile +++ b/packages/mcp-server/Dockerfile @@ -37,19 +37,11 @@ COPY . . RUN yarn install --frozen-lockfile && \ yarn build -FROM denoland/deno:bin-2.6.10 AS deno_installer -FROM gcr.io/distroless/cc@sha256:66d87e170bc2c5e2b8cf853501141c3c55b4e502b8677595c57534df54a68cc5 AS cc +FROM denoland/deno:alpine-2.7.1 -FROM node:24-alpine +# Install node and npm +RUN apk add --no-cache nodejs npm -# Install deno -COPY --from=deno_installer /deno /usr/local/bin/deno - -# Add in shared libraries needed by Deno -COPY --from=cc /lib/*-linux-gnu/* /usr/local/lib/ -COPY --from=cc /lib/ld-linux-* /lib/ - -RUN mkdir /lib64 && ln -s /usr/local/lib/ld-linux-* /lib64/ ENV LD_LIBRARY_PATH=/usr/lib:/usr/local/lib # Add non-root user @@ -69,6 +61,7 @@ COPY --from=builder /build/dist ./node_modules/@imagekit/nodejs # Change ownership to nodejs user RUN chown -R nodejs:nodejs /app +RUN chown -R nodejs:nodejs /deno-dir # Switch to non-root user USER nodejs From 81ec737f908203323d163470fd3a86abbb79db27 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 11:24:01 +0000 Subject: [PATCH 29/50] feat(api): dpr type update --- .stats.yml | 4 ++-- src/resources/beta/v2/files.ts | 9 +++++---- src/resources/files/files.ts | 9 +++++---- src/resources/shared.ts | 2 +- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/.stats.yml b/.stats.yml index dbc39e12..7401c19f 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 47 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/imagekit-inc%2Fimagekit-13fc3d7cafdea492f62eef7c1d63424d6d9d8adbff74b9f6ca6fd3fc12a36840.yml -openapi_spec_hash: a1fe6fa48207791657a1ea2d60a6dfcc +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/imagekit-inc%2Fimagekit-e5b5a158784fff63311ceef956e8d5ce9e87c6b557d23ab737270040eb39adcd.yml +openapi_spec_hash: b0293985a8806f367319af573d1b418c config_hash: 47cb702ee2cb52c58d803ae39ade9b44 diff --git a/src/resources/beta/v2/files.ts b/src/resources/beta/v2/files.ts index 09d55b2c..f3003db6 100644 --- a/src/resources/beta/v2/files.ts +++ b/src/resources/beta/v2/files.ts @@ -22,10 +22,11 @@ export class Files extends APIResource { * about how to implement secure client-side file upload. * * **File size limit** \ - * On the free plan, the maximum upload file sizes are 20MB for images, audio, and raw - * files, and 100MB for videos. On the paid plan, these limits increase to 40MB for - * images, audio, and raw files, and 2GB for videos. These limits can be further increased - * with higher-tier plans. + * On the free plan, the maximum upload file sizes are 25MB for images, audio, and raw + * files, and 100MB for videos. On the Lite paid plan, these limits increase to 40MB + * for images, audio, and raw files and 300MB for videos, whereas on the Pro paid plan, + * these limits increase to 50MB for images, audio, and raw files and 2GB for videos. + * These limits can be further increased with enterprise plans. * * **Version limit** \ * A file can have a maximum of 100 versions. diff --git a/src/resources/files/files.ts b/src/resources/files/files.ts index 229a3cfe..1025de95 100644 --- a/src/resources/files/files.ts +++ b/src/resources/files/files.ts @@ -152,10 +152,11 @@ export class Files extends APIResource { * by verifying the entire payload using JWT. * * **File size limit** \ - * On the free plan, the maximum upload file sizes are 20MB for images, audio, and raw - * files and 100MB for videos. On the paid plan, these limits increase to 40MB for images, - * audio, and raw files and 2GB for videos. These limits can be further increased with - * higher-tier plans. + * On the free plan, the maximum upload file sizes are 25MB for images, audio, and raw + * files and 100MB for videos. On the Lite paid plan, these limits increase to 40MB + * for images, audio, and raw files and 300MB for videos, whereas on the Pro paid plan, + * these limits increase to 50MB for images, audio, and raw files and 2GB for videos. + * These limits can be further increased with enterprise plans. * * **Version limit** \ * A file can have a maximum of 100 versions. diff --git a/src/resources/shared.ts b/src/resources/shared.ts index 2a4d9643..cbf290ee 100644 --- a/src/resources/shared.ts +++ b/src/resources/shared.ts @@ -1454,7 +1454,7 @@ export interface Transformation { * [Arithmetic expressions](https://imagekit.io/docs/arithmetic-expressions-in-transformations). * - See [DPR](https://imagekit.io/docs/image-resize-and-crop#dpr---dpr). */ - dpr?: number; + dpr?: number | string; /** * Specifies the duration (in seconds) for trimming videos, e.g., `5` or `10.5`. From 60f7ea6ff1946645f7e65ab93b0c77c4db250f83 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 16:01:05 +0000 Subject: [PATCH 30/50] chore(internal): move stringifyQuery implementation to internal function --- src/client.ts | 22 +++++----------------- src/internal/utils.ts | 1 + src/internal/utils/query.ts | 23 +++++++++++++++++++++++ tests/stringifyQuery.test.ts | 6 ++---- 4 files changed, 31 insertions(+), 21 deletions(-) create mode 100644 src/internal/utils/query.ts diff --git a/src/client.ts b/src/client.ts index 9b293fb3..cae93b95 100644 --- a/src/client.ts +++ b/src/client.ts @@ -11,6 +11,7 @@ import type { APIResponseProps } from './internal/parse'; import { getPlatformHeaders } from './internal/detect-platform'; import * as Shims from './internal/shims'; import * as Opts from './internal/request-options'; +import { stringifyQuery } from './internal/utils/query'; import { VERSION } from './version'; import * as Errors from './core/error'; import * as Uploads from './core/uploads'; @@ -323,21 +324,8 @@ export class ImageKit { /** * Basic re-implementation of `qs.stringify` for primitive types. */ - protected stringifyQuery(query: Record): string { - return Object.entries(query) - .filter(([_, value]) => typeof value !== 'undefined') - .map(([key, value]) => { - if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { - return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`; - } - if (value === null) { - return `${encodeURIComponent(key)}=`; - } - throw new Errors.ImageKitError( - `Cannot stringify type ${typeof value}; Expected string, number, boolean, or null. If you need to pass nested query parameters, you can manually encode them, e.g. { query: { 'foo[key1]': value1, 'foo[key2]': value2 } }, and please open a GitHub issue requesting better support for your use case.`, - ); - }) - .join('&'); + protected stringifyQuery(query: object | Record): string { + return stringifyQuery(query); } private getUserAgent(): string { @@ -374,7 +362,7 @@ export class ImageKit { } if (typeof query === 'object' && query && !Array.isArray(query)) { - url.search = this.stringifyQuery(query as Record); + url.search = this.stringifyQuery(query); } return url.toString(); @@ -813,7 +801,7 @@ export class ImageKit { ) { return { bodyHeaders: { 'content-type': 'application/x-www-form-urlencoded' }, - body: this.stringifyQuery(body as Record), + body: this.stringifyQuery(body), }; } else { return this.#encoder({ body, headers }); diff --git a/src/internal/utils.ts b/src/internal/utils.ts index 3cbfacce..c591353b 100644 --- a/src/internal/utils.ts +++ b/src/internal/utils.ts @@ -6,3 +6,4 @@ export * from './utils/env'; export * from './utils/log'; export * from './utils/uuid'; export * from './utils/sleep'; +export * from './utils/query'; diff --git a/src/internal/utils/query.ts b/src/internal/utils/query.ts new file mode 100644 index 00000000..530162a7 --- /dev/null +++ b/src/internal/utils/query.ts @@ -0,0 +1,23 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { ImageKitError } from '../../core/error'; + +/** + * Basic re-implementation of `qs.stringify` for primitive types. + */ +export function stringifyQuery(query: object | Record) { + return Object.entries(query) + .filter(([_, value]) => typeof value !== 'undefined') + .map(([key, value]) => { + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { + return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`; + } + if (value === null) { + return `${encodeURIComponent(key)}=`; + } + throw new ImageKitError( + `Cannot stringify type ${typeof value}; Expected string, number, boolean, or null. If you need to pass nested query parameters, you can manually encode them, e.g. { query: { 'foo[key1]': value1, 'foo[key2]': value2 } }, and please open a GitHub issue requesting better support for your use case.`, + ); + }) + .join('&'); +} diff --git a/tests/stringifyQuery.test.ts b/tests/stringifyQuery.test.ts index c3193151..4fa1d747 100644 --- a/tests/stringifyQuery.test.ts +++ b/tests/stringifyQuery.test.ts @@ -1,8 +1,6 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { ImageKit } from '@imagekit/nodejs'; - -const { stringifyQuery } = ImageKit.prototype as any; +import { stringifyQuery } from '@imagekit/nodejs/internal/utils/query'; describe(stringifyQuery, () => { for (const [input, expected] of [ @@ -15,7 +13,7 @@ describe(stringifyQuery, () => { 'e=f', )}=${encodeURIComponent('g&h')}`, ], - ]) { + ] as const) { it(`${JSON.stringify(input)} -> ${expected}`, () => { expect(stringifyQuery(input)).toEqual(expected); }); From af87a8ba4f06ca786ec611177663e21f85e2f8d4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 17:57:40 +0000 Subject: [PATCH 31/50] feat(mcp): add an option to disable code tool You can now disable the code tool via a flag, in case your server only needs access to the search_docs capability (useful for coding agents). --- packages/mcp-server/src/options.ts | 11 ++++++++++- packages/mcp-server/src/server.ts | 16 ++++++++++------ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/packages/mcp-server/src/options.ts b/packages/mcp-server/src/options.ts index 9e9d15cd..069b8811 100644 --- a/packages/mcp-server/src/options.ts +++ b/packages/mcp-server/src/options.ts @@ -14,6 +14,7 @@ export type CLIOptions = McpOptions & { }; export type McpOptions = { + includeCodeTool?: boolean | undefined; includeDocsTools?: boolean | undefined; stainlessApiKey?: string | undefined; codeAllowHttpGets?: boolean | undefined; @@ -92,11 +93,13 @@ export function parseCLIOptions(): CLIOptions { : argv.tools?.includes(toolType) ? true : undefined; + const includeCodeTool = shouldIncludeToolType('code'); const includeDocsTools = shouldIncludeToolType('docs'); const transport = argv.transport as 'stdio' | 'http'; return { + ...(includeCodeTool !== undefined && { includeCodeTool }), ...(includeDocsTools !== undefined && { includeDocsTools }), debug: !!argv.debug, stainlessApiKey: argv.stainlessApiKey, @@ -129,13 +132,19 @@ export function parseQueryOptions(defaultOptions: McpOptions, query: unknown): M const queryObject = typeof query === 'string' ? qs.parse(query) : query; const queryOptions = QueryOptions.parse(queryObject); + let codeTool: boolean | undefined = + queryOptions.no_tools && queryOptions.no_tools?.includes('code') ? false + : queryOptions.tools?.includes('code') ? true + : defaultOptions.includeCodeTool; + let docsTools: boolean | undefined = queryOptions.no_tools && queryOptions.no_tools?.includes('docs') ? false : queryOptions.tools?.includes('docs') ? true : defaultOptions.includeDocsTools; return { - codeExecutionMode: defaultOptions.codeExecutionMode, + ...(codeTool !== undefined && { includeCodeTool: codeTool }), ...(docsTools !== undefined && { includeDocsTools: docsTools }), + codeExecutionMode: defaultOptions.codeExecutionMode, }; } diff --git a/packages/mcp-server/src/server.ts b/packages/mcp-server/src/server.ts index d17fa460..17244a07 100644 --- a/packages/mcp-server/src/server.ts +++ b/packages/mcp-server/src/server.ts @@ -156,12 +156,16 @@ export async function initMcpServer(params: { * Selects the tools to include in the MCP Server based on the provided options. */ export function selectTools(options?: McpOptions): McpTool[] { - const includedTools = [ - codeTool({ - blockedMethods: blockedMethodsForCodeTool(options), - codeExecutionMode: options?.codeExecutionMode ?? 'stainless-sandbox', - }), - ]; + const includedTools = []; + + if (options?.includeCodeTool ?? true) { + includedTools.push( + codeTool({ + blockedMethods: blockedMethodsForCodeTool(options), + codeExecutionMode: options?.codeExecutionMode ?? 'stainless-sandbox', + }), + ); + } if (options?.includeDocsTools ?? true) { includedTools.push(docsSearchTool); } From db925be3e3cae3ab8cb5e4f4b8f34909a21791e7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 21:28:59 +0000 Subject: [PATCH 32/50] fix(mcp): update prompt updates the agent prompt to prevent a common TypeScript error when accessing object keys --- packages/mcp-server/src/code-tool.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/mcp-server/src/code-tool.ts b/packages/mcp-server/src/code-tool.ts index f93ca950..9a8b4289 100644 --- a/packages/mcp-server/src/code-tool.ts +++ b/packages/mcp-server/src/code-tool.ts @@ -23,7 +23,7 @@ import { ClientOptions } from '@imagekit/nodejs'; const prompt = `Runs JavaScript code to interact with the Image Kit API. -You are a skilled programmer writing code to interface with the service. +You are a skilled TypeScript programmer writing code to interface with the service. Define an async function named "run" that takes a single parameter of an initialized SDK client and it will be run. For example: @@ -39,7 +39,9 @@ You will be returned anything that your function returns, plus the results of an Do not add try-catch blocks for single API calls. The tool will handle errors for you. Do not add comments unless necessary for generating better code. Code will run in a container, and cannot interact with the network outside of the given SDK client. -Variables will not persist between calls, so make sure to return or log any data you might need later.`; +Variables will not persist between calls, so make sure to return or log any data you might need later. +Remember that you are writing TypeScript code, so you need to be careful with your types. +Always type dynamic key-value stores explicitly as Record instead of {}.`; /** * A tool that runs code against a copy of the SDK. From f08d3006d05cc29718533591605500ff6edd12f7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 28 Feb 2026 06:53:21 +0000 Subject: [PATCH 33/50] chore(internal): switch MCP servers to use pino for logging --- packages/mcp-server/package.json | 6 +- packages/mcp-server/src/code-tool.ts | 23 ++++++- packages/mcp-server/src/docs-search-tool.ts | 30 ++++++++- packages/mcp-server/src/http.ts | 74 +++++++++++++++------ packages/mcp-server/src/index.ts | 26 ++++---- packages/mcp-server/src/instructions.ts | 3 +- packages/mcp-server/src/logger.ts | 28 ++++++++ packages/mcp-server/src/options.ts | 11 +++ packages/mcp-server/src/stdio.ts | 3 +- 9 files changed, 161 insertions(+), 43 deletions(-) create mode 100644 packages/mcp-server/src/logger.ts diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json index cffedd65..fc41006e 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -39,8 +39,9 @@ "express": "^5.1.0", "fuse.js": "^7.1.0", "jq-web": "https://github.com/stainless-api/jq-web/releases/download/v0.8.8/jq-web.tar.gz", - "morgan": "^1.10.0", - "morgan-body": "^2.6.9", + "pino": "^10.3.1", + "pino-http": "^11.0.0", + "pino-pretty": "^13.1.3", "qs": "^6.14.1", "typescript": "5.8.3", "yargs": "^17.7.2", @@ -57,7 +58,6 @@ "@types/cors": "^2.8.19", "@types/express": "^5.0.3", "@types/jest": "^29.4.0", - "@types/morgan": "^1.9.10", "@types/qs": "^6.14.0", "@types/yargs": "^17.0.8", "@typescript-eslint/eslint-plugin": "8.31.1", diff --git a/packages/mcp-server/src/code-tool.ts b/packages/mcp-server/src/code-tool.ts index 9a8b4289..75a2d1af 100644 --- a/packages/mcp-server/src/code-tool.ts +++ b/packages/mcp-server/src/code-tool.ts @@ -17,6 +17,7 @@ import { import { Tool } from '@modelcontextprotocol/sdk/types.js'; import { readEnv, requireValue } from './util'; import { WorkerInput, WorkerOutput } from './code-tool-types'; +import { getLogger } from './logger'; import { SdkMethod } from './methods'; import { McpCodeExecutionMode } from './options'; import { ClientOptions } from '@imagekit/nodejs'; @@ -83,6 +84,8 @@ export function codeTool({ }, }; + const logger = getLogger(); + const handler = async ({ reqContext, args, @@ -107,11 +110,27 @@ export function codeTool({ } } + let result: ToolCallResult; + const startTime = Date.now(); + if (codeExecutionMode === 'local') { - return await localDenoHandler({ reqContext, args }); + logger.debug('Executing code in local Deno environment'); + result = await localDenoHandler({ reqContext, args }); } else { - return await remoteStainlessHandler({ reqContext, args }); + logger.debug('Executing code in remote Stainless environment'); + result = await remoteStainlessHandler({ reqContext, args }); } + + logger.info( + { + codeExecutionMode, + durationMs: Date.now() - startTime, + isError: result.isError, + contentRows: result.content?.length ?? 0, + }, + 'Got code tool execution result', + ); + return result; }; return { metadata, tool, handler }; diff --git a/packages/mcp-server/src/docs-search-tool.ts b/packages/mcp-server/src/docs-search-tool.ts index 6d78bb6b..e87268d2 100644 --- a/packages/mcp-server/src/docs-search-tool.ts +++ b/packages/mcp-server/src/docs-search-tool.ts @@ -1,7 +1,8 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { Metadata, McpRequestContext, asTextContentResult } from './types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; +import { Metadata, McpRequestContext, asTextContentResult } from './types'; +import { getLogger } from './logger'; export const metadata: Metadata = { resource: 'all', @@ -50,19 +51,42 @@ export const handler = async ({ }) => { const body = args as any; const query = new URLSearchParams(body).toString(); + + const startTime = Date.now(); const result = await fetch(`${docsSearchURL}?${query}`, { headers: { ...(reqContext.stainlessApiKey && { Authorization: reqContext.stainlessApiKey }), }, }); + const logger = getLogger(); + if (!result.ok) { + const errorText = await result.text(); + logger.warn( + { + durationMs: Date.now() - startTime, + query: body.query, + status: result.status, + statusText: result.statusText, + errorText, + }, + 'Got error response from docs search tool', + ); throw new Error( - `${result.status}: ${result.statusText} when using doc search tool. Details: ${await result.text()}`, + `${result.status}: ${result.statusText} when using doc search tool. Details: ${errorText}`, ); } - return asTextContentResult(await result.json()); + const resultBody = await result.json(); + logger.info( + { + durationMs: Date.now() - startTime, + query: body.query, + }, + 'Got docs search result', + ); + return asTextContentResult(resultBody); }; export default { metadata, tool, handler }; diff --git a/packages/mcp-server/src/http.ts b/packages/mcp-server/src/http.ts index f3354511..a5ef0dd0 100644 --- a/packages/mcp-server/src/http.ts +++ b/packages/mcp-server/src/http.ts @@ -4,9 +4,10 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import { ClientOptions } from '@imagekit/nodejs'; import express from 'express'; -import morgan from 'morgan'; -import morganBody from 'morgan-body'; +import pino from 'pino'; +import pinoHttp from 'pino-http'; import { getStainlessApiKey, parseClientAuthHeaders } from './auth'; +import { getLogger } from './logger'; import { McpOptions } from './options'; import { initMcpServer, newMcpServer } from './server'; @@ -70,29 +71,60 @@ const del = async (req: express.Request, res: express.Response) => { }); }; +const redactHeaders = (headers: Record) => { + const hiddenHeaders = /auth|cookie|key|token/i; + const filtered = { ...headers }; + Object.keys(filtered).forEach((key) => { + if (hiddenHeaders.test(key)) { + filtered[key] = '[REDACTED]'; + } + }); + return filtered; +}; + export const streamableHTTPApp = ({ clientOptions = {}, mcpOptions, - debug, }: { clientOptions?: ClientOptions; mcpOptions: McpOptions; - debug: boolean; }): express.Express => { const app = express(); app.set('query parser', 'extended'); app.use(express.json()); - - if (debug) { - morganBody(app, { - logAllReqHeader: true, - logAllResHeader: true, - logRequestBody: true, - logResponseBody: true, - }); - } else { - app.use(morgan('combined')); - } + app.use( + pinoHttp({ + logger: getLogger(), + customLogLevel: (req, res) => { + if (res.statusCode >= 500) { + return 'error'; + } else if (res.statusCode >= 400) { + return 'warn'; + } + return 'info'; + }, + customSuccessMessage: function (req, res) { + return `Request ${req.method} to ${req.url} completed with status ${res.statusCode}`; + }, + customErrorMessage: function (req, res, err) { + return `Request ${req.method} to ${req.url} errored with status ${res.statusCode}`; + }, + serializers: { + req: pino.stdSerializers.wrapRequestSerializer((req) => { + return { + ...req, + headers: redactHeaders(req.raw.headers), + }; + }), + res: pino.stdSerializers.wrapResponseSerializer((res) => { + return { + ...res, + headers: redactHeaders(res.headers), + }; + }), + }, + }), + ); app.get('/health', async (req: express.Request, res: express.Response) => { res.status(200).send('OK'); @@ -106,22 +138,22 @@ export const streamableHTTPApp = ({ export const launchStreamableHTTPServer = async ({ mcpOptions, - debug, port, }: { mcpOptions: McpOptions; - debug: boolean; port: number | string | undefined; }) => { - const app = streamableHTTPApp({ mcpOptions, debug }); + const app = streamableHTTPApp({ mcpOptions }); const server = app.listen(port); const address = server.address(); + const logger = getLogger(); + if (typeof address === 'string') { - console.error(`MCP Server running on streamable HTTP at ${address}`); + logger.info(`MCP Server running on streamable HTTP at ${address}`); } else if (address !== null) { - console.error(`MCP Server running on streamable HTTP on port ${address.port}`); + logger.info(`MCP Server running on streamable HTTP on port ${address.port}`); } else { - console.error(`MCP Server running on streamable HTTP on port ${port}`); + logger.info(`MCP Server running on streamable HTTP on port ${port}`); } }; diff --git a/packages/mcp-server/src/index.ts b/packages/mcp-server/src/index.ts index 654d25cf..5bca4a60 100644 --- a/packages/mcp-server/src/index.ts +++ b/packages/mcp-server/src/index.ts @@ -5,15 +5,20 @@ import { McpOptions, parseCLIOptions } from './options'; import { launchStdioServer } from './stdio'; import { launchStreamableHTTPServer } from './http'; import type { McpTool } from './types'; +import { configureLogger, getLogger } from './logger'; async function main() { const options = parseOptionsOrError(); + configureLogger({ + level: options.debug ? 'debug' : 'info', + pretty: options.logFormat === 'pretty', + }); const selectedTools = await selectToolsOrError(options); - console.error( - `MCP Server starting with ${selectedTools.length} tools:`, - selectedTools.map((e) => e.tool.name), + getLogger().info( + { tools: selectedTools.map((e) => e.tool.name) }, + `MCP Server starting with ${selectedTools.length} tools`, ); switch (options.transport) { @@ -23,7 +28,6 @@ async function main() { case 'http': await launchStreamableHTTPServer({ mcpOptions: options, - debug: options.debug, port: options.socket ?? options.port, }); break; @@ -32,7 +36,8 @@ async function main() { if (require.main === module) { main().catch((error) => { - console.error('Fatal error in main():', error); + // Logger might not be initialized yet + console.error('Fatal error in main()', error); process.exit(1); }); } @@ -41,7 +46,8 @@ function parseOptionsOrError() { try { return parseCLIOptions(); } catch (error) { - console.error('Error parsing options:', error); + // Logger is initialized after options, so use console.error here + console.error('Error parsing options', error); process.exit(1); } } @@ -50,16 +56,12 @@ async function selectToolsOrError(options: McpOptions): Promise { try { const includedTools = selectTools(options); if (includedTools.length === 0) { - console.error('No tools match the provided filters.'); + getLogger().error('No tools match the provided filters'); process.exit(1); } return includedTools; } catch (error) { - if (error instanceof Error) { - console.error('Error filtering tools:', error.message); - } else { - console.error('Error filtering tools:', error); - } + getLogger().error({ error }, 'Error filtering tools'); process.exit(1); } } diff --git a/packages/mcp-server/src/instructions.ts b/packages/mcp-server/src/instructions.ts index 9b2b945a..24cf912d 100644 --- a/packages/mcp-server/src/instructions.ts +++ b/packages/mcp-server/src/instructions.ts @@ -1,6 +1,7 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. import { readEnv } from './util'; +import { getLogger } from './logger'; const INSTRUCTIONS_CACHE_TTL_MS = 15 * 60 * 1000; // 15 minutes @@ -50,7 +51,7 @@ async function fetchLatestInstructions(stainlessApiKey: string | undefined): Pro let instructions: string | undefined; if (!response.ok) { - console.warn( + getLogger().warn( 'Warning: failed to retrieve MCP server instructions. Proceeding with default instructions...', ); diff --git a/packages/mcp-server/src/logger.ts b/packages/mcp-server/src/logger.ts new file mode 100644 index 00000000..29dab11c --- /dev/null +++ b/packages/mcp-server/src/logger.ts @@ -0,0 +1,28 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { pino, type Level, type Logger } from 'pino'; +import pretty from 'pino-pretty'; + +let _logger: Logger | undefined; + +export function configureLogger({ level, pretty: usePretty }: { level: Level; pretty: boolean }): void { + _logger = pino( + { + level, + timestamp: pino.stdTimeFunctions.isoTime, + formatters: { + level(label) { + return { level: label }; + }, + }, + }, + usePretty ? pretty({ colorize: true, levelFirst: true, destination: 2 }) : process.stderr, + ); +} + +export function getLogger(): Logger { + if (!_logger) { + throw new Error('Logger has not been configured. Call configureLogger() before using the logger.'); + } + return _logger; +} diff --git a/packages/mcp-server/src/options.ts b/packages/mcp-server/src/options.ts index 069b8811..b9e8e8a6 100644 --- a/packages/mcp-server/src/options.ts +++ b/packages/mcp-server/src/options.ts @@ -8,6 +8,7 @@ import { readEnv } from './util'; export type CLIOptions = McpOptions & { debug: boolean; + logFormat: 'json' | 'pretty'; transport: 'stdio' | 'http'; port: number | undefined; socket: string | undefined; @@ -52,6 +53,11 @@ export function parseCLIOptions(): CLIOptions { "Where to run code execution in code tool; 'stainless-sandbox' will execute code in Stainless-hosted sandboxes whereas 'local' will execute code locally on the MCP server machine.", }) .option('debug', { type: 'boolean', description: 'Enable debug logging' }) + .option('log-format', { + type: 'string', + choices: ['json', 'pretty'], + description: 'Format for log output; defaults to json unless tty is detected', + }) .option('no-tools', { type: 'string', array: true, @@ -97,6 +103,10 @@ export function parseCLIOptions(): CLIOptions { const includeDocsTools = shouldIncludeToolType('docs'); const transport = argv.transport as 'stdio' | 'http'; + const logFormat = + argv.logFormat ? (argv.logFormat as 'json' | 'pretty') + : process.stderr.isTTY ? 'pretty' + : 'json'; return { ...(includeCodeTool !== undefined && { includeCodeTool }), @@ -108,6 +118,7 @@ export function parseCLIOptions(): CLIOptions { codeBlockedMethods: argv.codeBlockedMethods, codeExecutionMode: argv.codeExecutionMode as McpCodeExecutionMode, transport, + logFormat, port: argv.port, socket: argv.socket, }; diff --git a/packages/mcp-server/src/stdio.ts b/packages/mcp-server/src/stdio.ts index ceccaed3..e8bcbb19 100644 --- a/packages/mcp-server/src/stdio.ts +++ b/packages/mcp-server/src/stdio.ts @@ -1,6 +1,7 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { McpOptions } from './options'; import { initMcpServer, newMcpServer } from './server'; +import { getLogger } from './logger'; export const launchStdioServer = async (mcpOptions: McpOptions) => { const server = await newMcpServer(mcpOptions.stainlessApiKey); @@ -9,5 +10,5 @@ export const launchStdioServer = async (mcpOptions: McpOptions) => { const transport = new StdioServerTransport(); await server.connect(transport); - console.error('MCP Server running on stdio'); + getLogger().info('MCP Server running on stdio'); }; From f179fa9536d097bcaed31310deeadbfc74457814 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 3 Mar 2026 00:26:44 +0000 Subject: [PATCH 34/50] chore(tests): update webhook tests --- tests/api-resources/webhooks.test.ts | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/tests/api-resources/webhooks.test.ts b/tests/api-resources/webhooks.test.ts index 8fd83de9..e5547f28 100644 --- a/tests/api-resources/webhooks.test.ts +++ b/tests/api-resources/webhooks.test.ts @@ -11,13 +11,13 @@ const client = new ImageKit({ }); describe('resource webhooks', () => { - test.skip('unwrap', async () => { + test.skip('unwrap', () => { const key = 'whsec_c2VjcmV0Cg=='; const payload = '{"id":"id","type":"video.transformation.accepted","created_at":"2019-12-27T18:11:19.117Z","data":{"asset":{"url":"https://example.com"},"transformation":{"type":"video-transformation","options":{"audio_codec":"aac","auto_rotate":true,"format":"mp4","quality":0,"stream_protocol":"HLS","variants":["string"],"video_codec":"h264"}}},"request":{"url":"https://example.com","x_request_id":"x_request_id","user_agent":"user_agent"}}'; const msgID = '1'; const timestamp = new Date(); - const wh = new Webhook(key); + const wh = new Webhook('whsec_c2VjcmV0Cg=='); const signature = wh.sign(msgID, timestamp, payload); const headers: Record = { 'webhook-signature': signature, @@ -25,19 +25,41 @@ describe('resource webhooks', () => { 'webhook-timestamp': String(Math.floor(timestamp.getTime() / 1000)), }; client.webhooks.unwrap(payload, { headers, key }); + client.withOptions({ webhookSecret: key }).webhooks.unwrap(payload, { headers }); + client.withOptions({ webhookSecret: 'whsec_aaaaaaaaaa==' }).webhooks.unwrap(payload, { headers, key }); expect(() => { const wrongKey = 'whsec_aaaaaaaaaa=='; client.webhooks.unwrap(payload, { headers, key: wrongKey }); }).toThrow('No matching signature found'); + expect(() => { + const wrongKey = 'whsec_aaaaaaaaaa=='; + client.withOptions({ webhookSecret: wrongKey }).webhooks.unwrap(payload, { headers }); + }).toThrow('No matching signature found'); expect(() => { const badSig = wh.sign(msgID, timestamp, 'some other payload'); client.webhooks.unwrap(payload, { headers: { ...headers, 'webhook-signature': badSig }, key }); }).toThrow('No matching signature found'); + expect(() => { + const badSig = wh.sign(msgID, timestamp, 'some other payload'); + client + .withOptions({ webhookSecret: key }) + .webhooks.unwrap(payload, { headers: { ...headers, 'webhook-signature': badSig } }); + }).toThrow('No matching signature found'); expect(() => { client.webhooks.unwrap(payload, { headers: { ...headers, 'webhook-timestamp': '5' }, key }); }).toThrow('Message timestamp too old'); + expect(() => { + client + .withOptions({ webhookSecret: key }) + .webhooks.unwrap(payload, { headers: { ...headers, 'webhook-timestamp': '5' } }); + }).toThrow('Message timestamp too old'); expect(() => { client.webhooks.unwrap(payload, { headers: { ...headers, 'webhook-id': 'wrong' }, key }); }).toThrow('No matching signature found'); + expect(() => { + client + .withOptions({ webhookSecret: key }) + .webhooks.unwrap(payload, { headers: { ...headers, 'webhook-id': 'wrong' } }); + }).toThrow('No matching signature found'); }); }); From 568dd2ae4a631c3b64812d1b13af5e6aaba6ebd1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 3 Mar 2026 01:04:27 +0000 Subject: [PATCH 35/50] chore(mcp-server): return access instructions for 404 without API key --- packages/mcp-server/src/code-tool.ts | 5 +++++ packages/mcp-server/src/docs-search-tool.ts | 7 +++++++ 2 files changed, 12 insertions(+) diff --git a/packages/mcp-server/src/code-tool.ts b/packages/mcp-server/src/code-tool.ts index 75a2d1af..884ec28a 100644 --- a/packages/mcp-server/src/code-tool.ts +++ b/packages/mcp-server/src/code-tool.ts @@ -175,6 +175,11 @@ const remoteStainlessHandler = async ({ }); if (!res.ok) { + if (res.status === 404 && !reqContext.stainlessApiKey) { + throw new Error( + 'Could not access code tool for this project. You may need to provide a Stainless API key via the STAINLESS_API_KEY environment variable, the --stainless-api-key flag, or the x-stainless-api-key HTTP header.', + ); + } throw new Error( `${res.status}: ${ res.statusText diff --git a/packages/mcp-server/src/docs-search-tool.ts b/packages/mcp-server/src/docs-search-tool.ts index e87268d2..b6fc4770 100644 --- a/packages/mcp-server/src/docs-search-tool.ts +++ b/packages/mcp-server/src/docs-search-tool.ts @@ -73,6 +73,13 @@ export const handler = async ({ }, 'Got error response from docs search tool', ); + + if (result.status === 404 && !reqContext.stainlessApiKey) { + throw new Error( + 'Could not find docs for this project. You may need to provide a Stainless API key via the STAINLESS_API_KEY environment variable, the --stainless-api-key flag, or the x-stainless-api-key HTTP header.', + ); + } + throw new Error( `${result.status}: ${result.statusText} when using doc search tool. Details: ${errorText}`, ); From d128af71272254b0f6fbc52029e941e674b594cf Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 4 Mar 2026 17:37:11 +0000 Subject: [PATCH 36/50] chore(internal): use x-stainless-mcp-client-envs header for MCP remote code tool calls --- packages/mcp-server/src/code-tool.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mcp-server/src/code-tool.ts b/packages/mcp-server/src/code-tool.ts index 884ec28a..8a81295e 100644 --- a/packages/mcp-server/src/code-tool.ts +++ b/packages/mcp-server/src/code-tool.ts @@ -155,7 +155,7 @@ const remoteStainlessHandler = async ({ headers: { ...(reqContext.stainlessApiKey && { Authorization: reqContext.stainlessApiKey }), 'Content-Type': 'application/json', - client_envs: JSON.stringify({ + 'x-stainless-mcp-client-envs': JSON.stringify({ IMAGEKIT_PRIVATE_KEY: requireValue( readEnv('IMAGEKIT_PRIVATE_KEY') ?? client.privateKey, 'set IMAGEKIT_PRIVATE_KEY environment variable or provide privateKey client option', From 2543278ccc78081ffa4cbd82dd1061310c480529 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 03:18:12 +0000 Subject: [PATCH 37/50] feat(api): revert dpr breaking change --- .stats.yml | 4 ++-- src/resources/shared.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.stats.yml b/.stats.yml index 7401c19f..749d6988 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 47 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/imagekit-inc%2Fimagekit-e5b5a158784fff63311ceef956e8d5ce9e87c6b557d23ab737270040eb39adcd.yml -openapi_spec_hash: b0293985a8806f367319af573d1b418c +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/imagekit-inc%2Fimagekit-83a7f3659a437113f2a79e1e72794be19eff00ec232fd0206198c80364ccfebf.yml +openapi_spec_hash: b327552548ab641eb4ea3b45e643dfce config_hash: 47cb702ee2cb52c58d803ae39ade9b44 diff --git a/src/resources/shared.ts b/src/resources/shared.ts index cbf290ee..2a4d9643 100644 --- a/src/resources/shared.ts +++ b/src/resources/shared.ts @@ -1454,7 +1454,7 @@ export interface Transformation { * [Arithmetic expressions](https://imagekit.io/docs/arithmetic-expressions-in-transformations). * - See [DPR](https://imagekit.io/docs/image-resize-and-crop#dpr---dpr). */ - dpr?: number | string; + dpr?: number; /** * Specifies the duration (in seconds) for trimming videos, e.g., `5` or `10.5`. From 8cc484fda37d8a3188f10f3dfa1cd9ab291f10f6 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 09:25:45 +0000 Subject: [PATCH 38/50] fix: fix request delays for retrying to be more respectful of high requested delays --- src/client.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client.ts b/src/client.ts index cae93b95..94f52476 100644 --- a/src/client.ts +++ b/src/client.ts @@ -667,9 +667,9 @@ export class ImageKit { } } - // If the API asks us to wait a certain amount of time (and it's a reasonable amount), - // just do what it says, but otherwise calculate a default - if (!(timeoutMillis && 0 <= timeoutMillis && timeoutMillis < 60 * 1000)) { + // If the API asks us to wait a certain amount of time, just do what it + // says, but otherwise calculate a default + if (timeoutMillis === undefined) { const maxRetries = options.maxRetries ?? this.maxRetries; timeoutMillis = this.calculateDefaultRetryTimeoutMillis(retriesRemaining, maxRetries); } From 7d86a36233f5ea27342329f6545d2b46eddbd219 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 7 Mar 2026 13:01:24 +0000 Subject: [PATCH 39/50] chore(ci): skip uploading artifacts on stainless-internal branches --- .github/workflows/ci.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6e879421..9e900aa1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,14 +55,18 @@ jobs: run: ./scripts/build - name: Get GitHub OIDC Token - if: github.repository == 'stainless-sdks/imagekit-typescript' + if: |- + github.repository == 'stainless-sdks/imagekit-typescript' && + !startsWith(github.ref, 'refs/heads/stl/') id: github-oidc uses: actions/github-script@v8 with: script: core.setOutput('github_token', await core.getIDToken()); - name: Upload tarball - if: github.repository == 'stainless-sdks/imagekit-typescript' + if: |- + github.repository == 'stainless-sdks/imagekit-typescript' && + !startsWith(github.ref, 'refs/heads/stl/') env: URL: https://pkg.stainless.com/s AUTH: ${{ steps.github-oidc.outputs.github_token }} @@ -70,7 +74,9 @@ jobs: run: ./scripts/utils/upload-artifact.sh - name: Upload MCP Server tarball - if: github.repository == 'stainless-sdks/imagekit-typescript' + if: |- + github.repository == 'stainless-sdks/imagekit-typescript' && + !startsWith(github.ref, 'refs/heads/stl/') env: URL: https://pkg.stainless.com/s?subpackage=mcp-server AUTH: ${{ steps.github-oidc.outputs.github_token }} From f8f62ae1890416cb5322c7d7b7ee2235b252983b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 7 Mar 2026 13:18:06 +0000 Subject: [PATCH 40/50] chore: update placeholder string --- tests/api-resources/beta/v2/files.test.ts | 4 ++-- tests/api-resources/files/files.test.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/api-resources/beta/v2/files.test.ts b/tests/api-resources/beta/v2/files.test.ts index 6d1d5b84..69af3768 100644 --- a/tests/api-resources/beta/v2/files.test.ts +++ b/tests/api-resources/beta/v2/files.test.ts @@ -12,7 +12,7 @@ describe('resource files', () => { // Mock server tests are disabled test.skip('upload: only required params', async () => { const responsePromise = client.beta.v2.files.upload({ - file: await toFile(Buffer.from('# my file contents'), 'README.md'), + file: await toFile(Buffer.from('Example data'), 'README.md'), fileName: 'fileName', }); const rawResponse = await responsePromise.asResponse(); @@ -27,7 +27,7 @@ describe('resource files', () => { // Mock server tests are disabled test.skip('upload: required and optional params', async () => { const response = await client.beta.v2.files.upload({ - file: await toFile(Buffer.from('# my file contents'), 'README.md'), + file: await toFile(Buffer.from('Example data'), 'README.md'), fileName: 'fileName', token: 'token', checks: '"request.folder" : "marketing/"\n', diff --git a/tests/api-resources/files/files.test.ts b/tests/api-resources/files/files.test.ts index ff75998c..aeb90697 100644 --- a/tests/api-resources/files/files.test.ts +++ b/tests/api-resources/files/files.test.ts @@ -119,7 +119,7 @@ describe('resource files', () => { // Mock server tests are disabled test.skip('upload: only required params', async () => { const responsePromise = client.files.upload({ - file: await toFile(Buffer.from('# my file contents'), 'README.md'), + file: await toFile(Buffer.from('Example data'), 'README.md'), fileName: 'fileName', }); const rawResponse = await responsePromise.asResponse(); @@ -134,7 +134,7 @@ describe('resource files', () => { // Mock server tests are disabled test.skip('upload: required and optional params', async () => { const response = await client.files.upload({ - file: await toFile(Buffer.from('# my file contents'), 'README.md'), + file: await toFile(Buffer.from('Example data'), 'README.md'), fileName: 'fileName', token: 'token', checks: '"request.folder" : "marketing/"\n', From 9fed3c543ed9c115c50f56d22a52bff307cc6d08 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 7 Mar 2026 13:56:14 +0000 Subject: [PATCH 41/50] fix(client): preserve URL params already embedded in path --- src/client.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/client.ts b/src/client.ts index 94f52476..7be4e749 100644 --- a/src/client.ts +++ b/src/client.ts @@ -357,8 +357,9 @@ export class ImageKit { : new URL(baseURL + (baseURL.endsWith('/') && path.startsWith('/') ? path.slice(1) : path)); const defaultQuery = this.defaultQuery(); - if (!isEmptyObj(defaultQuery)) { - query = { ...defaultQuery, ...query }; + const pathQuery = Object.fromEntries(url.searchParams); + if (!isEmptyObj(defaultQuery) || !isEmptyObj(pathQuery)) { + query = { ...pathQuery, ...defaultQuery, ...query }; } if (typeof query === 'object' && query && !Array.isArray(query)) { From 9afa574d4589f6229784ba766e604a0498f506a3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 7 Mar 2026 14:59:31 +0000 Subject: [PATCH 42/50] chore(mcp-server): improve instructions --- packages/mcp-server/src/docs-search-tool.ts | 3 ++- packages/mcp-server/src/instructions.ts | 14 ++------------ 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/packages/mcp-server/src/docs-search-tool.ts b/packages/mcp-server/src/docs-search-tool.ts index b6fc4770..4ab3e63b 100644 --- a/packages/mcp-server/src/docs-search-tool.ts +++ b/packages/mcp-server/src/docs-search-tool.ts @@ -13,7 +13,8 @@ export const metadata: Metadata = { export const tool: Tool = { name: 'search_docs', - description: 'Search for documentation for how to use the client to interact with the API.', + description: + 'Search SDK documentation to find methods, parameters, and usage examples for interacting with the API. Use this before writing code when you need to discover the right approach.', inputSchema: { type: 'object', properties: { diff --git a/packages/mcp-server/src/instructions.ts b/packages/mcp-server/src/instructions.ts index 24cf912d..15e875d9 100644 --- a/packages/mcp-server/src/instructions.ts +++ b/packages/mcp-server/src/instructions.ts @@ -55,21 +55,11 @@ async function fetchLatestInstructions(stainlessApiKey: string | undefined): Pro 'Warning: failed to retrieve MCP server instructions. Proceeding with default instructions...', ); - instructions = ` - This is the imagekit MCP server. You will use Code Mode to help the user perform - actions. You can use search_docs tool to learn about how to take action with this server. Then, - you will write TypeScript code using the execute tool take action. It is CRITICAL that you be - thoughtful and deliberate when executing code. Always try to entirely solve the problem in code - block: it can be as long as you need to get the job done! - `; + instructions = + '\n This is the imagekit MCP server.\n\n Available tools:\n - search_docs: Search SDK documentation to find the right methods and parameters.\n - execute: Run TypeScript code against a pre-authenticated SDK client. Define an async run(client) function.\n\n Workflow:\n - If unsure about the API, call search_docs first.\n - Write complete solutions in a single execute call when possible. For large datasets, use API filters to narrow results or paginate within a single execute block.\n - If execute returns an error, read the error and fix your code rather than retrying the same approach.\n - Variables do not persist between execute calls. Return or log all data you need.\n - Individual HTTP requests to the API have a 30-second timeout. If a request times out, try a smaller query or add filters.\n - Code execution has a total timeout of approximately 5 minutes. If your code times out, simplify it or break it into smaller steps.\n '; } instructions ??= ((await response.json()) as { instructions: string }).instructions; - instructions = ` - If needed, you can get the current time by executing Date.now(). - - ${instructions} - `; return instructions; } From c6f8d113f12ef419981dfb5df68208c079f1970b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 9 Mar 2026 21:36:05 +0000 Subject: [PATCH 43/50] chore(internal): update dependencies to address dependabot vulnerabilities --- package.json | 11 ++++++ .../mcp-server/cloudflare-worker/package.json | 6 +-- packages/mcp-server/package.json | 15 ++++--- yarn.lock | 39 +++---------------- 4 files changed, 29 insertions(+), 42 deletions(-) diff --git a/package.json b/package.json index 2c422387..69664751 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,17 @@ "typescript": "5.8.3", "typescript-eslint": "8.31.1" }, + "overrides": { + "minimatch": "^9.0.5" + }, + "pnpm": { + "overrides": { + "minimatch": "^9.0.5" + } + }, + "resolutions": { + "minimatch": "^9.0.5" + }, "exports": { ".": { "import": "./dist/index.mjs", diff --git a/packages/mcp-server/cloudflare-worker/package.json b/packages/mcp-server/cloudflare-worker/package.json index 8f711420..04f555bc 100644 --- a/packages/mcp-server/cloudflare-worker/package.json +++ b/packages/mcp-server/cloudflare-worker/package.json @@ -18,9 +18,9 @@ }, "dependencies": { "@cloudflare/workers-oauth-provider": "^0.0.5", - "@modelcontextprotocol/sdk": "^1.26.0", - "agents": "^0.0.113", - "hono": "^4.12.2", + "@modelcontextprotocol/sdk": "^1.27.1", + "agents": "^0.3.10", + "hono": "^4.12.4", "@imagekit/api-mcp": "latest", "zod": "^3.24.4" } diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json index fc41006e..66ee93da 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -26,13 +26,16 @@ "format": "prettier --write --cache --cache-strategy metadata . !dist", "prepare": "npm run build", "tsn": "ts-node -r tsconfig-paths/register", - "lint": "eslint --ext ts,js .", - "fix": "eslint --fix --ext ts,js ." + "lint": "eslint .", + "fix": "eslint --fix ." }, "dependencies": { "@imagekit/nodejs": "file:../../dist/", + "ajv": "^8.18.0", "@cloudflare/cabidela": "^0.2.4", - "@modelcontextprotocol/sdk": "^1.26.0", + "@hono/node-server": "^1.19.10", + "@modelcontextprotocol/sdk": "^1.27.1", + "hono": "^4.12.4", "@valtown/deno-http-worker": "^0.0.21", "cookie-parser": "^1.4.6", "cors": "^2.8.5", @@ -62,9 +65,9 @@ "@types/yargs": "^17.0.8", "@typescript-eslint/eslint-plugin": "8.31.1", "@typescript-eslint/parser": "8.31.1", - "eslint": "^8.49.0", - "eslint-plugin-prettier": "^5.0.1", - "eslint-plugin-unused-imports": "^3.0.0", + "eslint": "^9.39.1", + "eslint-plugin-prettier": "^5.4.1", + "eslint-plugin-unused-imports": "^4.1.4", "jest": "^29.4.0", "prettier": "^3.0.0", "ts-jest": "^29.1.0", diff --git a/yarn.lock b/yarn.lock index 2dc54a9d..49adad97 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1224,15 +1224,7 @@ baseline-browser-mapping@^2.9.0: resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.9.14.tgz#3b6af0bc032445bca04de58caa9a87cfe921cbb3" integrity sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg== -brace-expansion@^1.1.7: - version "1.1.12" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843" - integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -brace-expansion@^2.0.1: +brace-expansion@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.2.tgz#54fc53237a613d854c7bd37463aad17df87214e7" integrity sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ== @@ -1400,11 +1392,6 @@ commander@^10.0.1: resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== - convert-source-map@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" @@ -2610,26 +2597,12 @@ mimic-fn@^2.1.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== -minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== - dependencies: - brace-expansion "^1.1.7" - -minimatch@^5.0.1: - version "5.1.6" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" - integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== - dependencies: - brace-expansion "^2.0.1" - -minimatch@^9.0.4: - version "9.0.5" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" - integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== +minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2, minimatch@^5.0.1, minimatch@^9.0.4, minimatch@^9.0.5: + version "9.0.9" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.9.tgz#9b0cb9fcb78087f6fd7eababe2511c4d3d60574e" + integrity sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg== dependencies: - brace-expansion "^2.0.1" + brace-expansion "^2.0.2" minimist@^1.2.6: version "1.2.6" From a0031e0a7be090912d18950ae5acffcef7b416e5 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 11 Mar 2026 18:50:08 +0000 Subject: [PATCH 44/50] chore(internal): bump @modelcontextprotocol/sdk, @hono/node-server, and minimatch --- packages/mcp-server/yarn.lock | 549 +++++++++++++++++++++++----------- 1 file changed, 382 insertions(+), 167 deletions(-) diff --git a/packages/mcp-server/yarn.lock b/packages/mcp-server/yarn.lock index fe102775..c7e37692 100644 --- a/packages/mcp-server/yarn.lock +++ b/packages/mcp-server/yarn.lock @@ -297,61 +297,101 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" -"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": +"@eslint-community/eslint-utils@^4.4.0", "@eslint-community/eslint-utils@^4.8.0": version "4.9.1" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz#4e90af67bc51ddee6cdef5284edf572ec376b595" integrity sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ== dependencies: eslint-visitor-keys "^3.4.3" -"@eslint-community/regexpp@^4.10.0", "@eslint-community/regexpp@^4.6.1": +"@eslint-community/regexpp@^4.10.0", "@eslint-community/regexpp@^4.12.1": version "4.12.2" resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz#bccdf615bcf7b6e8db830ec0b8d21c9a25de597b" integrity sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew== -"@eslint/eslintrc@^2.1.4": - version "2.1.4" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad" - integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ== +"@eslint/config-array@^0.21.2": + version "0.21.2" + resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.21.2.tgz#f29e22057ad5316cf23836cee9a34c81fffcb7e6" + integrity sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw== dependencies: - ajv "^6.12.4" + "@eslint/object-schema" "^2.1.7" + debug "^4.3.1" + minimatch "^3.1.5" + +"@eslint/config-helpers@^0.4.2": + version "0.4.2" + resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.4.2.tgz#1bd006ceeb7e2e55b2b773ab318d300e1a66aeda" + integrity sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw== + dependencies: + "@eslint/core" "^0.17.0" + +"@eslint/core@^0.17.0": + version "0.17.0" + resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.17.0.tgz#77225820413d9617509da9342190a2019e78761c" + integrity sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ== + dependencies: + "@types/json-schema" "^7.0.15" + +"@eslint/eslintrc@^3.3.5": + version "3.3.5" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.3.5.tgz#c131793cfc1a7b96f24a83e0a8bbd4b881558c60" + integrity sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg== + dependencies: + ajv "^6.14.0" debug "^4.3.2" - espree "^9.6.0" - globals "^13.19.0" + espree "^10.0.1" + globals "^14.0.0" ignore "^5.2.0" import-fresh "^3.2.1" - js-yaml "^4.1.0" - minimatch "^3.1.2" + js-yaml "^4.1.1" + minimatch "^3.1.5" strip-json-comments "^3.1.1" -"@eslint/js@8.57.1": - version "8.57.1" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2" - integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q== +"@eslint/js@9.39.4": + version "9.39.4" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.39.4.tgz#a3f83bfc6fd9bf33a853dfacd0b49b398eb596c1" + integrity sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw== -"@hono/node-server@^1.19.7": - version "1.19.9" - resolved "https://registry.yarnpkg.com/@hono/node-server/-/node-server-1.19.9.tgz#8f37119b1acf283fd3f6035f3d1356fdb97a09ac" - integrity sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw== +"@eslint/object-schema@^2.1.7": + version "2.1.7" + resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.7.tgz#6e2126a1347e86a4dedf8706ec67ff8e107ebbad" + integrity sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA== -"@humanwhocodes/config-array@^0.13.0": - version "0.13.0" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz#fb907624df3256d04b9aa2df50d7aa97ec648748" - integrity sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw== +"@eslint/plugin-kit@^0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz#9779e3fd9b7ee33571a57435cf4335a1794a6cb2" + integrity sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA== dependencies: - "@humanwhocodes/object-schema" "^2.0.3" - debug "^4.3.1" - minimatch "^3.0.5" + "@eslint/core" "^0.17.0" + levn "^0.4.1" + +"@hono/node-server@^1.19.10", "@hono/node-server@^1.19.9": + version "1.19.11" + resolved "https://registry.yarnpkg.com/@hono/node-server/-/node-server-1.19.11.tgz#dc419f0826dd2504e9fc86ad289d5636a0444e2f" + integrity sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g== + +"@humanfs/core@^0.19.1": + version "0.19.1" + resolved "https://registry.yarnpkg.com/@humanfs/core/-/core-0.19.1.tgz#17c55ca7d426733fe3c561906b8173c336b40a77" + integrity sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA== + +"@humanfs/node@^0.16.6": + version "0.16.7" + resolved "https://registry.yarnpkg.com/@humanfs/node/-/node-0.16.7.tgz#822cb7b3a12c5a240a24f621b5a2413e27a45f26" + integrity sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ== + dependencies: + "@humanfs/core" "^0.19.1" + "@humanwhocodes/retry" "^0.4.0" "@humanwhocodes/module-importer@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== -"@humanwhocodes/object-schema@^2.0.3": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" - integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== +"@humanwhocodes/retry@^0.4.0", "@humanwhocodes/retry@^0.4.2": + version "0.4.3" + resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.4.3.tgz#c2b9d2e374ee62c586d3adbea87199b1d7a7a6ba" + integrity sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ== "@inquirer/checkbox@^3.0.1": version "3.0.1" @@ -741,12 +781,12 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@modelcontextprotocol/sdk@^1.25.2": - version "1.25.2" - resolved "https://registry.yarnpkg.com/@modelcontextprotocol/sdk/-/sdk-1.25.2.tgz#2284560b4e044b4ce5f328ee180931110cb8c5cf" - integrity sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww== +"@modelcontextprotocol/sdk@^1.27.1": + version "1.27.1" + resolved "https://registry.yarnpkg.com/@modelcontextprotocol/sdk/-/sdk-1.27.1.tgz#a602cf823bf8a68e13e7112f50aeb02b09fb83b9" + integrity sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA== dependencies: - "@hono/node-server" "^1.19.7" + "@hono/node-server" "^1.19.9" ajv "^8.17.1" ajv-formats "^3.0.1" content-type "^1.0.5" @@ -754,14 +794,15 @@ cross-spawn "^7.0.5" eventsource "^3.0.2" eventsource-parser "^3.0.0" - express "^5.0.1" - express-rate-limit "^7.5.0" - jose "^6.1.1" + express "^5.2.1" + express-rate-limit "^8.2.1" + hono "^4.11.4" + jose "^6.1.3" json-schema-typed "^8.0.2" pkce-challenge "^5.0.0" raw-body "^3.0.0" zod "^3.25 || ^4.0" - zod-to-json-schema "^3.25.0" + zod-to-json-schema "^3.25.1" "@nodelib/fs.scandir@2.1.5": version "2.1.5" @@ -776,7 +817,7 @@ resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== -"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": +"@nodelib/fs.walk@^1.2.3": version "1.2.8" resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== @@ -784,6 +825,11 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@pinojs/redact@^0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@pinojs/redact/-/redact-0.4.0.tgz#c3de060dd12640dcc838516aa2a6803cc7b2e9d6" + integrity sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg== + "@pkgr/core@^0.2.9": version "0.2.9" resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.2.9.tgz#d229a7b7f9dac167a156992ef23c7f023653f53b" @@ -886,6 +932,11 @@ dependencies: "@types/node" "*" +"@types/cookie-parser@^1.4.10": + version "1.4.10" + resolved "https://registry.yarnpkg.com/@types/cookie-parser/-/cookie-parser-1.4.10.tgz#a045272a383a30597a01955d4f9c790018f214e4" + integrity sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg== + "@types/cors@^2.8.19": version "2.8.19" resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.19.tgz#d93ea2673fd8c9f697367f5eeefc2bbfa94f0342" @@ -893,6 +944,11 @@ dependencies: "@types/node" "*" +"@types/estree@^1.0.6": + version "1.0.8" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e" + integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== + "@types/express-serve-static-core@^5.0.0": version "5.1.1" resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz#1a77faffee9572d39124933259be2523837d7eaa" @@ -951,6 +1007,11 @@ expect "^29.0.0" pretty-format "^29.0.0" +"@types/json-schema@^7.0.15": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + "@types/mute-stream@^0.0.4": version "0.0.4" resolved "https://registry.yarnpkg.com/@types/mute-stream/-/mute-stream-0.0.4.tgz#77208e56a08767af6c5e1237be8888e2f255c478" @@ -1100,11 +1161,6 @@ "@typescript-eslint/types" "8.31.1" eslint-visitor-keys "^4.2.0" -"@ungap/structured-clone@^1.2.0": - version "1.3.0" - resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz#d06bbb384ebcf6c505fde1c3d0ed4ddffe0aaff8" - integrity sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g== - "@valtown/deno-http-worker@^0.0.21": version "0.0.21" resolved "https://registry.yarnpkg.com/@valtown/deno-http-worker/-/deno-http-worker-0.0.21.tgz#9ce3b5c1d0db211fe7ea8297881fe551838474ad" @@ -1130,11 +1186,16 @@ acorn-walk@^8.1.1: dependencies: acorn "^8.11.0" -acorn@^8.11.0, acorn@^8.4.1, acorn@^8.9.0: +acorn@^8.11.0, acorn@^8.4.1: version "8.15.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816" integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== +acorn@^8.15.0: + version "8.16.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.16.0.tgz#4ce79c89be40afe7afe8f3adb902a1f1ce9ac08a" + integrity sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw== + aggregate-error@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" @@ -1150,10 +1211,10 @@ ajv-formats@^3.0.1: dependencies: ajv "^8.0.0" -ajv@^6.12.4: - version "6.12.6" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" - integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== +ajv@^6.14.0: + version "6.14.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.14.0.tgz#fd067713e228210636ebb08c60bd3765d6dbe73a" + integrity sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw== dependencies: fast-deep-equal "^3.1.1" fast-json-stable-stringify "^2.0.0" @@ -1170,6 +1231,16 @@ ajv@^8.0.0, ajv@^8.17.1: json-schema-traverse "^1.0.0" require-from-string "^2.0.2" +ajv@^8.18.0: + version "8.18.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.18.0.tgz#8864186b6738d003eb3a933172bb3833e10cefbc" + integrity sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A== + dependencies: + fast-deep-equal "^3.1.3" + fast-uri "^3.0.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + ansi-escapes@^4.2.1, ansi-escapes@^4.3.2: version "4.3.2" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" @@ -1219,6 +1290,11 @@ argparse@^2.0.1: resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== +atomic-sleep@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b" + integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ== + babel-jest@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.7.0.tgz#f4369919225b684c56085998ac63dbd05be020d5" @@ -1474,6 +1550,11 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +colorette@^2.0.7: + version "2.0.20" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" + integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== + commander@^13.1.0: version "13.1.0" resolved "https://registry.yarnpkg.com/commander/-/commander-13.1.0.tgz#776167db68c78f38dcce1f9b8d7b8b9a488abf46" @@ -1499,12 +1580,25 @@ convert-source-map@^2.0.0: resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== +cookie-parser@^1.4.6: + version "1.4.7" + resolved "https://registry.yarnpkg.com/cookie-parser/-/cookie-parser-1.4.7.tgz#e2125635dfd766888ffe90d60c286404fa0e7b26" + integrity sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw== + dependencies: + cookie "0.7.2" + cookie-signature "1.0.6" + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + cookie-signature@^1.2.1: version "1.2.2" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.2.2.tgz#57c7fc3cc293acab9fec54d73e15690ebe4a1793" integrity sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg== -cookie@^0.7.1: +cookie@0.7.2, cookie@^0.7.1: version "0.7.2" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7" integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w== @@ -1535,7 +1629,7 @@ create-require@^1.1.0: resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== -cross-spawn@^7.0.2, cross-spawn@^7.0.3, cross-spawn@^7.0.5: +cross-spawn@^7.0.3, cross-spawn@^7.0.5, cross-spawn@^7.0.6: version "7.0.6" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== @@ -1544,6 +1638,11 @@ cross-spawn@^7.0.2, cross-spawn@^7.0.3, cross-spawn@^7.0.5: shebang-command "^2.0.0" which "^2.0.1" +dateformat@^4.6.3: + version "4.6.3" + resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-4.6.3.tgz#556fa6497e5217fedb78821424f8a1c22fa3f4b5" + integrity sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA== + debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.7, debug@^4.4.0, debug@^4.4.3: version "4.4.3" resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" @@ -1586,13 +1685,6 @@ diff@^4.0.1: resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== -doctrine@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" - integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== - dependencies: - esutils "^2.0.2" - dunder-proto@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" @@ -1627,6 +1719,13 @@ encodeurl@^2.0.0: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== +end-of-stream@^1.1.0: + version "1.4.5" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.5.tgz#7344d711dea40e0b74abc2ed49778743ccedb08c" + integrity sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg== + dependencies: + once "^1.4.0" + error-ex@^1.3.1: version "1.3.4" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.4.tgz#b3a8d8bb6f92eecc1629e3e27d3c8607a8a32414" @@ -1671,7 +1770,7 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== -eslint-plugin-prettier@^5.0.1: +eslint-plugin-prettier@^5.4.1: version "5.5.5" resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.5.tgz#9eae11593faa108859c26f9a9c367d619a0769c0" integrity sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw== @@ -1679,95 +1778,84 @@ eslint-plugin-prettier@^5.0.1: prettier-linter-helpers "^1.0.1" synckit "^0.11.12" -eslint-plugin-unused-imports@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-3.2.0.tgz#63a98c9ad5f622cd9f830f70bc77739f25ccfe0d" - integrity sha512-6uXyn6xdINEpxE1MtDjxQsyXB37lfyO2yKGVVgtD7WEWQGORSOZjgrD6hBhvGv4/SO+TOlS+UnC6JppRqbuwGQ== - dependencies: - eslint-rule-composer "^0.3.0" - -eslint-rule-composer@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz#79320c927b0c5c0d3d3d2b76c8b4a488f25bbaf9" - integrity sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg== +eslint-plugin-unused-imports@^4.1.4: + version "4.4.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.4.1.tgz#a831f0a2937d7631eba30cb87091ab7d3a5da0e1" + integrity sha512-oZGYUz1X3sRMGUB+0cZyK2VcvRX5lm/vB56PgNNcU+7ficUCKm66oZWKUubXWnOuPjQ8PvmXtCViXBMONPe7tQ== -eslint-scope@^7.2.2: - version "7.2.2" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" - integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== +eslint-scope@^8.4.0: + version "8.4.0" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-8.4.0.tgz#88e646a207fad61436ffa39eb505147200655c82" + integrity sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg== dependencies: esrecurse "^4.3.0" estraverse "^5.2.0" -eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: +eslint-visitor-keys@^3.4.3: version "3.4.3" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== -eslint-visitor-keys@^4.2.0: +eslint-visitor-keys@^4.2.0, eslint-visitor-keys@^4.2.1: version "4.2.1" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz#4cfea60fe7dd0ad8e816e1ed026c1d5251b512c1" integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ== -eslint@^8.49.0: - version "8.57.1" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.1.tgz#7df109654aba7e3bbe5c8eae533c5e461d3c6ca9" - integrity sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA== - dependencies: - "@eslint-community/eslint-utils" "^4.2.0" - "@eslint-community/regexpp" "^4.6.1" - "@eslint/eslintrc" "^2.1.4" - "@eslint/js" "8.57.1" - "@humanwhocodes/config-array" "^0.13.0" +eslint@^9.39.1: + version "9.39.4" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.39.4.tgz#855da1b2e2ad66dc5991195f35e262bcec8117b5" + integrity sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ== + dependencies: + "@eslint-community/eslint-utils" "^4.8.0" + "@eslint-community/regexpp" "^4.12.1" + "@eslint/config-array" "^0.21.2" + "@eslint/config-helpers" "^0.4.2" + "@eslint/core" "^0.17.0" + "@eslint/eslintrc" "^3.3.5" + "@eslint/js" "9.39.4" + "@eslint/plugin-kit" "^0.4.1" + "@humanfs/node" "^0.16.6" "@humanwhocodes/module-importer" "^1.0.1" - "@nodelib/fs.walk" "^1.2.8" - "@ungap/structured-clone" "^1.2.0" - ajv "^6.12.4" + "@humanwhocodes/retry" "^0.4.2" + "@types/estree" "^1.0.6" + ajv "^6.14.0" chalk "^4.0.0" - cross-spawn "^7.0.2" + cross-spawn "^7.0.6" debug "^4.3.2" - doctrine "^3.0.0" escape-string-regexp "^4.0.0" - eslint-scope "^7.2.2" - eslint-visitor-keys "^3.4.3" - espree "^9.6.1" - esquery "^1.4.2" + eslint-scope "^8.4.0" + eslint-visitor-keys "^4.2.1" + espree "^10.4.0" + esquery "^1.5.0" esutils "^2.0.2" fast-deep-equal "^3.1.3" - file-entry-cache "^6.0.1" + file-entry-cache "^8.0.0" find-up "^5.0.0" glob-parent "^6.0.2" - globals "^13.19.0" - graphemer "^1.4.0" ignore "^5.2.0" imurmurhash "^0.1.4" is-glob "^4.0.0" - is-path-inside "^3.0.3" - js-yaml "^4.1.0" json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.4.1" lodash.merge "^4.6.2" - minimatch "^3.1.2" + minimatch "^3.1.5" natural-compare "^1.4.0" optionator "^0.9.3" - strip-ansi "^6.0.1" - text-table "^0.2.0" -espree@^9.6.0, espree@^9.6.1: - version "9.6.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" - integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== +espree@^10.0.1, espree@^10.4.0: + version "10.4.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-10.4.0.tgz#d54f4949d4629005a1fa168d937c3ff1f7e2a837" + integrity sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ== dependencies: - acorn "^8.9.0" + acorn "^8.15.0" acorn-jsx "^5.3.2" - eslint-visitor-keys "^3.4.1" + eslint-visitor-keys "^4.2.1" esprima@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== -esquery@^1.4.2: +esquery@^1.5.0: version "1.7.0" resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.7.0.tgz#08d048f261f0ddedb5bae95f46809463d9c9496d" integrity sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g== @@ -1839,12 +1927,14 @@ expect@^29.0.0, expect@^29.7.0: jest-message-util "^29.7.0" jest-util "^29.7.0" -express-rate-limit@^7.5.0: - version "7.5.1" - resolved "https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-7.5.1.tgz#8c3a42f69209a3a1c969890070ece9e20a879dec" - integrity sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw== +express-rate-limit@^8.2.1: + version "8.3.1" + resolved "https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-8.3.1.tgz#0aaba098eadd40f6737f30a98e6b16fa1a29edfb" + integrity sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw== + dependencies: + ip-address "10.1.0" -express@^5.0.1, express@^5.1.0: +express@^5.1.0, express@^5.2.1: version "5.2.1" resolved "https://registry.yarnpkg.com/express/-/express-5.2.1.tgz#8f21d15b6d327f92b4794ecf8cb08a72f956ac04" integrity sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw== @@ -1887,6 +1977,11 @@ external-editor@^3.1.0: iconv-lite "^0.4.24" tmp "^0.0.33" +fast-copy@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/fast-copy/-/fast-copy-4.0.2.tgz#57f14115e1edbec274f69090072a480aa29cbedd" + integrity sha512-ybA6PDXIXOXivLJK/z9e+Otk7ve13I4ckBvGO5I2RRmBU1gMHLVDJYEuJYhGwez7YNlYji2M2DvVU+a9mSFDlw== + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -1918,6 +2013,11 @@ fast-levenshtein@^2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== +fast-safe-stringify@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" + integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== + fast-uri@^3.0.1: version "3.1.0" resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.0.tgz#66eecff6c764c0df9b762e62ca7edcfb53b4edfa" @@ -1942,12 +2042,12 @@ fflate@^0.8.2: resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.8.2.tgz#fc8631f5347812ad6028bbe4a2308b2792aa1dea" integrity sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A== -file-entry-cache@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" - integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== +file-entry-cache@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz#7787bddcf1131bffb92636c69457bbc0edd6d81f" + integrity sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ== dependencies: - flat-cache "^3.0.4" + flat-cache "^4.0.0" fill-range@^7.1.1: version "7.1.1" @@ -1984,14 +2084,13 @@ find-up@^5.0.0: locate-path "^6.0.0" path-exists "^4.0.0" -flat-cache@^3.0.4: - version "3.2.0" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee" - integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw== +flat-cache@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-4.0.1.tgz#0ece39fcb14ee012f4b0410bd33dd9c1f011127c" + integrity sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw== dependencies: flatted "^3.2.9" - keyv "^4.5.3" - rimraf "^3.0.2" + keyv "^4.5.4" flatted@^3.2.9: version "3.3.3" @@ -2129,12 +2228,10 @@ glob@^7.1.3, glob@^7.1.4: once "^1.3.0" path-is-absolute "^1.0.0" -globals@^13.19.0: - version "13.24.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" - integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== - dependencies: - type-fest "^0.20.2" +globals@^14.0.0: + version "14.0.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e" + integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ== gopd@^1.2.0: version "1.2.0" @@ -2180,6 +2277,16 @@ hasown@^2.0.2: dependencies: function-bind "^1.1.2" +help-me@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/help-me/-/help-me-5.0.0.tgz#b1ebe63b967b74060027c2ac61f9be12d354a6f6" + integrity sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg== + +hono@^4.11.4, hono@^4.12.4: + version "4.12.7" + resolved "https://registry.yarnpkg.com/hono/-/hono-4.12.7.tgz#ca000956e965c2b3d791e43540498e616d6c6442" + integrity sha512-jq9l1DM0zVIvsm3lv9Nw9nlJnMNPOcAtsbsgiUhWcFzPE99Gvo6yRTlszSLLYacMeQ6quHD6hMfId8crVHvexw== + html-escaper@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" @@ -2264,6 +2371,11 @@ inherits@2, inherits@^2.0.3, inherits@~2.0.4: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== +ip-address@10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-10.1.0.tgz#d8dcffb34d0e02eb241427444a6e23f5b0595aa4" + integrity sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q== + ipaddr.js@1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" @@ -2308,11 +2420,6 @@ is-number@^7.0.0: resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== -is-path-inside@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" - integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== - is-promise@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-4.0.0.tgz#42ff9f84206c1991d26debf520dd5c01042dd2f3" @@ -2739,10 +2846,15 @@ jest@^29.4.0: import-local "^3.0.2" jest-cli "^29.7.0" -jose@^6.1.1: - version "6.1.3" - resolved "https://registry.yarnpkg.com/jose/-/jose-6.1.3.tgz#8453d7be88af7bb7d64a0481d6a35a0145ba3ea5" - integrity sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ== +jose@^6.1.3: + version "6.2.1" + resolved "https://registry.yarnpkg.com/jose/-/jose-6.2.1.tgz#7a6b1de83816deaee9055a558e1278a7b2b9ea1b" + integrity sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw== + +joycon@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03" + integrity sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw== "jq-web@https://github.com/stainless-api/jq-web/releases/download/v0.8.8/jq-web.tar.gz": version "0.8.8" @@ -2761,7 +2873,7 @@ js-yaml@^3.13.1: argparse "^1.0.7" esprima "^4.0.0" -js-yaml@^4.1.0: +js-yaml@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b" integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA== @@ -2817,7 +2929,7 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" -keyv@^4.5.3: +keyv@^4.5.4: version "4.5.4" resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== @@ -2947,10 +3059,10 @@ mimic-fn@^2.1.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== -minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== +minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.5.tgz#580c88f8d5445f2bd6aa8f3cadefa0de79fbd69e" + integrity sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w== dependencies: brace-expansion "^1.1.7" @@ -3040,6 +3152,11 @@ object-inspect@^1.13.3: resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== +on-exit-leak-free@^2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz#fed195c9ebddb7d9e4c3842f93f281ac8dadd3b8" + integrity sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA== + on-finished@^2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" @@ -3047,7 +3164,7 @@ on-finished@^2.4.1: dependencies: ee-first "1.1.1" -once@^1.3.0, once@^1.4.0: +once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== @@ -3187,6 +3304,64 @@ picomatch@^2.0.4, picomatch@^2.2.3, picomatch@^2.3.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== +pino-abstract-transport@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz#b21e5f33a297e8c4c915c62b3ce5dd4a87a52c23" + integrity sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg== + dependencies: + split2 "^4.0.0" + +pino-http@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/pino-http/-/pino-http-11.0.0.tgz#ebadef4694fc59aadab9be7e5939aea625b4615f" + integrity sha512-wqg5XIAGRRIWtTk8qPGxkbrfiwEWz1lgedVLvhLALudKXvg1/L2lTFgTGPJ4Z2e3qcRmxoFxDuSdMdMGNM6I1g== + dependencies: + get-caller-file "^2.0.5" + pino "^10.0.0" + pino-std-serializers "^7.0.0" + process-warning "^5.0.0" + +pino-pretty@^13.1.3: + version "13.1.3" + resolved "https://registry.yarnpkg.com/pino-pretty/-/pino-pretty-13.1.3.tgz#2274cccda925dd355c104079a5029f6598d0381b" + integrity sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg== + dependencies: + colorette "^2.0.7" + dateformat "^4.6.3" + fast-copy "^4.0.0" + fast-safe-stringify "^2.1.1" + help-me "^5.0.0" + joycon "^3.1.1" + minimist "^1.2.6" + on-exit-leak-free "^2.1.0" + pino-abstract-transport "^3.0.0" + pump "^3.0.0" + secure-json-parse "^4.0.0" + sonic-boom "^4.0.1" + strip-json-comments "^5.0.2" + +pino-std-serializers@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz#a7b0cd65225f29e92540e7853bd73b07479893fc" + integrity sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw== + +pino@^10.0.0, pino@^10.3.1: + version "10.3.1" + resolved "https://registry.yarnpkg.com/pino/-/pino-10.3.1.tgz#6552c8f8d8481844c9e452e7bf0be90bff1939ce" + integrity sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg== + dependencies: + "@pinojs/redact" "^0.4.0" + atomic-sleep "^1.0.0" + on-exit-leak-free "^2.1.0" + pino-abstract-transport "^3.0.0" + pino-std-serializers "^7.0.0" + process-warning "^5.0.0" + quick-format-unescaped "^4.0.3" + real-require "^0.2.0" + safe-stable-stringify "^2.3.1" + sonic-boom "^4.0.1" + thread-stream "^4.0.0" + pirates@^4.0.4: version "4.0.7" resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.7.tgz#643b4a18c4257c8a65104b73f3049ce9a0a15e22" @@ -3235,6 +3410,11 @@ pretty-format@^29.0.0, pretty-format@^29.7.0: ansi-styles "^5.0.0" react-is "^18.0.0" +process-warning@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-5.0.0.tgz#566e0bf79d1dff30a72d8bbbe9e8ecefe8d378d7" + integrity sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA== + prompts@^2.0.1: version "2.4.2" resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" @@ -3251,6 +3431,14 @@ proxy-addr@^2.0.7: forwarded "0.2.0" ipaddr.js "1.9.1" +pump@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.4.tgz#1f313430527fa8b905622ebd22fe1444e757ab3c" + integrity sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + punycode@^2.1.0: version "2.3.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" @@ -3273,6 +3461,11 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== +quick-format-unescaped@^4.0.3: + version "4.0.4" + resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz#93ef6dd8d3453cbc7970dd614fad4c5954d6b5a7" + integrity sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg== + range-parser@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" @@ -3302,6 +3495,11 @@ readable-stream@^3.4.0: string_decoder "^1.1.1" util-deprecate "^1.0.1" +real-require@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/real-require/-/real-require-0.2.0.tgz#209632dea1810be2ae063a6ac084fee7e33fba78" + integrity sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg== + require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" @@ -3348,13 +3546,6 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.1.0.tgz#0fe13b9522e1473f51b558ee796e08f11f9b489f" integrity sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw== -rimraf@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" - integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== - dependencies: - glob "^7.1.3" - router@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/router/-/router-2.2.0.tgz#019be620b711c87641167cc79b99090f00b146ef" @@ -3378,11 +3569,21 @@ safe-buffer@~5.2.0: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== +safe-stable-stringify@^2.3.1: + version "2.5.0" + resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz#4ca2f8e385f2831c432a719b108a3bf7af42a1dd" + integrity sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA== + "safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +secure-json-parse@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/secure-json-parse/-/secure-json-parse-4.1.0.tgz#4f1ab41c67a13497ea1b9131bb4183a22865477c" + integrity sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA== + semver@^6.3.0, semver@^6.3.1: version "6.3.1" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" @@ -3497,6 +3698,13 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +sonic-boom@^4.0.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-4.2.1.tgz#28598250df4899c0ac572d7e2f0460690ba6a030" + integrity sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q== + dependencies: + atomic-sleep "^1.0.0" + source-map-support@0.5.13: version "0.5.13" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" @@ -3510,6 +3718,11 @@ source-map@^0.6.0, source-map@^0.6.1: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== +split2@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4" + integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg== + sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" @@ -3585,6 +3798,11 @@ strip-json-comments@^3.1.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== +strip-json-comments@^5.0.2: + version "5.0.3" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-5.0.3.tgz#b7304249dd402ee67fd518ada993ab3593458bcf" + integrity sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw== + superstruct@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-1.0.4.tgz#0adb99a7578bd2f1c526220da6571b2d485d91ca" @@ -3625,10 +3843,12 @@ test-exclude@^6.0.0: glob "^7.1.4" minimatch "^3.0.4" -text-table@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" - integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== +thread-stream@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-4.0.0.tgz#732f007c24da7084f729d6e3a7e3f5934a7380b7" + integrity sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA== + dependencies: + real-require "^0.2.0" tmp@^0.0.33: version "0.0.33" @@ -3742,11 +3962,6 @@ type-detect@4.0.8: resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== -type-fest@^0.20.2: - version "0.20.2" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" - integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== - type-fest@^0.21.3: version "0.21.3" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" @@ -3933,7 +4148,7 @@ yoctocolors-cjs@^2.1.2: resolved "https://registry.yarnpkg.com/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz#7e4964ea8ec422b7a40ac917d3a344cfd2304baa" integrity sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw== -zod-to-json-schema@^3.24.5, zod-to-json-schema@^3.24.6, zod-to-json-schema@^3.25.0: +zod-to-json-schema@^3.24.5, zod-to-json-schema@^3.24.6, zod-to-json-schema@^3.25.1: version "3.25.1" resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz#7f24962101a439ddade2bf1aeab3c3bfec7d84ba" integrity sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA== From e6e8ed66ee0b76db104b7112c1540fa021b005ea Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 11 Mar 2026 20:02:29 +0000 Subject: [PATCH 45/50] chore: use proper capitalization for WebSockets --- .../cloudflare-worker/worker-configuration.d.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/mcp-server/cloudflare-worker/worker-configuration.d.ts b/packages/mcp-server/cloudflare-worker/worker-configuration.d.ts index e145ef08..432fe3e0 100644 --- a/packages/mcp-server/cloudflare-worker/worker-configuration.d.ts +++ b/packages/mcp-server/cloudflare-worker/worker-configuration.d.ts @@ -8214,7 +8214,7 @@ interface Ai_Cf_Deepgram_Flux_Input { tag?: string; } /** - * Output will be returned as websocket messages. + * Output will be returned as WebSocket messages. */ interface Ai_Cf_Deepgram_Flux_Output { /** @@ -8428,7 +8428,7 @@ type AiOptions = { */ queueRequest?: boolean; /** - * Establish websocket connections, only works for supported models + * Establish WebSocket connections, only works for supported models */ websocket?: boolean; /** @@ -9372,7 +9372,7 @@ interface IncomingRequestCfPropertiesTLSClientAuthPlaceholder { certNotAfter: ""; } /** Possible outcomes of TLS verification */ -declare type CertVerificationStatus = +declare type CertVerificationStatus = /** Authentication succeeded */ "SUCCESS" /** No certificate was presented */ @@ -9440,7 +9440,7 @@ interface D1ExecResult { count: number; duration: number; } -type D1SessionConstraint = +type D1SessionConstraint = // Indicates that the first query should go to the primary, and the rest queries // using the same D1DatabaseSession will go to any replica that is consistent with // the bookmark maintained by the session (returned by the first query). @@ -10061,7 +10061,7 @@ declare namespace Rpc { // The reason for using a generic type here is to build a serializable subset of structured // cloneable composite types. This allows types defined with the "interface" keyword to pass the // serializable check as well. Otherwise, only types defined with the "type" keyword would pass. - type Serializable = + type Serializable = // Structured cloneables BaseType // Structured cloneable composites @@ -10435,7 +10435,7 @@ declare namespace TailStream { } // This marks the worker handler return information. // This is separate from Outcome because the worker invocation can live for a long time after - // returning. For example - Websockets that return an http upgrade response but then continue + // returning. For example - WebSockets that return an http upgrade response but then continue // streaming information or SSE http connections. interface Return { readonly type: "return"; From 419e72f03c8854a3c6b6d4654ea474c3c926287c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 13 Mar 2026 16:23:53 +0000 Subject: [PATCH 46/50] chore(internal): make generated MCP servers compatible with Cloudflare worker environments --- packages/mcp-server/src/code-tool-paths.cts | 4 +++- packages/mcp-server/src/code-tool.ts | 12 +++++----- packages/mcp-server/src/instructions.ts | 25 +++++++++------------ 3 files changed, 20 insertions(+), 21 deletions(-) diff --git a/packages/mcp-server/src/code-tool-paths.cts b/packages/mcp-server/src/code-tool-paths.cts index 15ce7f55..78263e45 100644 --- a/packages/mcp-server/src/code-tool-paths.cts +++ b/packages/mcp-server/src/code-tool-paths.cts @@ -1,3 +1,5 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -export const workerPath = require.resolve('./code-tool-worker.mjs'); +export function getWorkerPath(): string { + return require.resolve('./code-tool-worker.mjs'); +} diff --git a/packages/mcp-server/src/code-tool.ts b/packages/mcp-server/src/code-tool.ts index 8a81295e..e07001c2 100644 --- a/packages/mcp-server/src/code-tool.ts +++ b/packages/mcp-server/src/code-tool.ts @@ -1,10 +1,5 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import fs from 'node:fs'; -import path from 'node:path'; -import url from 'node:url'; -import { newDenoHTTPWorker } from '@valtown/deno-http-worker'; -import { workerPath } from './code-tool-paths.cjs'; import { ContentBlock, McpRequestContext, @@ -207,6 +202,13 @@ const localDenoHandler = async ({ reqContext: McpRequestContext; args: unknown; }): Promise => { + const fs = await import('node:fs'); + const path = await import('node:path'); + const url = await import('node:url'); + const { newDenoHTTPWorker } = await import('@valtown/deno-http-worker'); + const { getWorkerPath } = await import('./code-tool-paths.cjs'); + const workerPath = getWorkerPath(); + const client = reqContext.client; const baseURLHostname = new URL(client.baseURL).hostname; const { code } = args as { code: string }; diff --git a/packages/mcp-server/src/instructions.ts b/packages/mcp-server/src/instructions.ts index 15e875d9..fd3c0711 100644 --- a/packages/mcp-server/src/instructions.ts +++ b/packages/mcp-server/src/instructions.ts @@ -12,29 +12,24 @@ interface InstructionsCacheEntry { const instructionsCache = new Map(); -// Periodically evict stale entries so the cache doesn't grow unboundedly. -const _cacheCleanupInterval = setInterval(() => { - const now = Date.now(); - for (const [key, entry] of instructionsCache) { - if (now - entry.fetchedAt > INSTRUCTIONS_CACHE_TTL_MS) { - instructionsCache.delete(key); - } - } -}, INSTRUCTIONS_CACHE_TTL_MS); - -// Don't keep the process alive just for cleanup. -_cacheCleanupInterval.unref(); - export async function getInstructions(stainlessApiKey: string | undefined): Promise { + const now = Date.now(); const cacheKey = stainlessApiKey ?? ''; const cached = instructionsCache.get(cacheKey); - if (cached && Date.now() - cached.fetchedAt <= INSTRUCTIONS_CACHE_TTL_MS) { + if (cached && now - cached.fetchedAt <= INSTRUCTIONS_CACHE_TTL_MS) { return cached.fetchedInstructions; } + // Evict stale entries so the cache doesn't grow unboundedly. + for (const [key, entry] of instructionsCache) { + if (now - entry.fetchedAt > INSTRUCTIONS_CACHE_TTL_MS) { + instructionsCache.delete(key); + } + } + const fetchedInstructions = await fetchLatestInstructions(stainlessApiKey); - instructionsCache.set(cacheKey, { fetchedInstructions, fetchedAt: Date.now() }); + instructionsCache.set(cacheKey, { fetchedInstructions, fetchedAt: now }); return fetchedInstructions; } From 6b2f37811676e4d1fdc24a175c477959b7aacde1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 13 Mar 2026 22:37:31 +0000 Subject: [PATCH 47/50] chore(internal): support x-stainless-mcp-client-envs header in MCP servers --- packages/mcp-server/src/code-tool.ts | 48 +++++++++++++++++----------- packages/mcp-server/src/http.ts | 16 +++++++++- packages/mcp-server/src/server.ts | 2 ++ packages/mcp-server/src/types.ts | 1 + 4 files changed, 47 insertions(+), 20 deletions(-) diff --git a/packages/mcp-server/src/code-tool.ts b/packages/mcp-server/src/code-tool.ts index e07001c2..1202a46e 100644 --- a/packages/mcp-server/src/code-tool.ts +++ b/packages/mcp-server/src/code-tool.ts @@ -144,22 +144,25 @@ const remoteStainlessHandler = async ({ const codeModeEndpoint = readEnv('CODE_MODE_ENDPOINT_URL') ?? 'https://api.stainless.com/api/ai/code-tool'; + const localClientEnvs = { + IMAGEKIT_PRIVATE_KEY: requireValue( + readEnv('IMAGEKIT_PRIVATE_KEY') ?? client.privateKey, + 'set IMAGEKIT_PRIVATE_KEY environment variable or provide privateKey client option', + ), + OPTIONAL_IMAGEKIT_IGNORES_THIS: readEnv('OPTIONAL_IMAGEKIT_IGNORES_THIS') ?? client.password ?? undefined, + IMAGEKIT_WEBHOOK_SECRET: readEnv('IMAGEKIT_WEBHOOK_SECRET') ?? client.webhookSecret ?? undefined, + IMAGE_KIT_BASE_URL: readEnv('IMAGE_KIT_BASE_URL') ?? client.baseURL ?? undefined, + }; + // Merge any upstream client envs from the request header, with upstream values taking precedence. + const mergedClientEnvs = { ...localClientEnvs, ...reqContext.upstreamClientEnvs }; + // Setting a Stainless API key authenticates requests to the code tool endpoint. const res = await fetch(codeModeEndpoint, { method: 'POST', headers: { ...(reqContext.stainlessApiKey && { Authorization: reqContext.stainlessApiKey }), 'Content-Type': 'application/json', - 'x-stainless-mcp-client-envs': JSON.stringify({ - IMAGEKIT_PRIVATE_KEY: requireValue( - readEnv('IMAGEKIT_PRIVATE_KEY') ?? client.privateKey, - 'set IMAGEKIT_PRIVATE_KEY environment variable or provide privateKey client option', - ), - OPTIONAL_IMAGEKIT_IGNORES_THIS: - readEnv('OPTIONAL_IMAGEKIT_IGNORES_THIS') ?? client.password ?? undefined, - IMAGEKIT_WEBHOOK_SECRET: readEnv('IMAGEKIT_WEBHOOK_SECRET') ?? client.webhookSecret ?? undefined, - IMAGE_KIT_BASE_URL: readEnv('IMAGE_KIT_BASE_URL') ?? client.baseURL ?? undefined, - }), + 'x-stainless-mcp-client-envs': JSON.stringify(mergedClientEnvs), }, body: JSON.stringify({ project_name: 'imagekit', @@ -270,6 +273,9 @@ const localDenoHandler = async ({ printOutput: true, spawnOptions: { cwd: path.dirname(workerPath), + // Merge any upstream client envs into the Deno subprocess environment, + // with the upstream env vars taking precedence. + env: { ...process.env, ...reqContext.upstreamClientEnvs }, }, }); @@ -279,15 +285,19 @@ const localDenoHandler = async ({ reject(new Error(`Worker exited with code ${exitCode}`)); }); - const opts: ClientOptions = { - baseURL: client.baseURL, - privateKey: client.privateKey, - password: client.password, - webhookSecret: client.webhookSecret, - defaultHeaders: { - 'X-Stainless-MCP': 'true', - }, - }; + // Strip null/undefined values so that the worker SDK client can fall back to + // reading from environment variables (including any upstreamClientEnvs). + const opts: ClientOptions = Object.fromEntries( + Object.entries({ + baseURL: client.baseURL, + privateKey: client.privateKey, + password: client.password, + webhookSecret: client.webhookSecret, + defaultHeaders: { + 'X-Stainless-MCP': 'true', + }, + }).filter(([_, v]) => v != null), + ) as ClientOptions; const req = worker.request( 'http://localhost', diff --git a/packages/mcp-server/src/http.ts b/packages/mcp-server/src/http.ts index a5ef0dd0..d6e14200 100644 --- a/packages/mcp-server/src/http.ts +++ b/packages/mcp-server/src/http.ts @@ -27,6 +27,19 @@ const newServer = async ({ const authOptions = parseClientAuthHeaders(req, false); + let upstreamClientEnvs: Record | undefined; + const clientEnvsHeader = req.headers['x-stainless-mcp-client-envs']; + if (typeof clientEnvsHeader === 'string') { + try { + const parsed = JSON.parse(clientEnvsHeader); + if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { + upstreamClientEnvs = parsed; + } + } catch { + // Ignore malformed header + } + } + await initMcpServer({ server: server, mcpOptions: mcpOptions, @@ -35,6 +48,7 @@ const newServer = async ({ ...authOptions, }, stainlessApiKey: stainlessApiKey, + upstreamClientEnvs, }); return server; @@ -72,7 +86,7 @@ const del = async (req: express.Request, res: express.Response) => { }; const redactHeaders = (headers: Record) => { - const hiddenHeaders = /auth|cookie|key|token/i; + const hiddenHeaders = /auth|cookie|key|token|x-stainless-mcp-client-envs/i; const filtered = { ...headers }; Object.keys(filtered).forEach((key) => { if (hiddenHeaders.test(key)) { diff --git a/packages/mcp-server/src/server.ts b/packages/mcp-server/src/server.ts index 17244a07..1bfb7083 100644 --- a/packages/mcp-server/src/server.ts +++ b/packages/mcp-server/src/server.ts @@ -37,6 +37,7 @@ export async function initMcpServer(params: { clientOptions?: ClientOptions; mcpOptions?: McpOptions; stainlessApiKey?: string | undefined; + upstreamClientEnvs?: Record | undefined; }) { const server = params.server instanceof McpServer ? params.server.server : params.server; @@ -118,6 +119,7 @@ export async function initMcpServer(params: { reqContext: { client, stainlessApiKey: params.stainlessApiKey ?? params.mcpOptions?.stainlessApiKey, + upstreamClientEnvs: params.upstreamClientEnvs, }, args, }); diff --git a/packages/mcp-server/src/types.ts b/packages/mcp-server/src/types.ts index afe343ee..1bf79bb6 100644 --- a/packages/mcp-server/src/types.ts +++ b/packages/mcp-server/src/types.ts @@ -45,6 +45,7 @@ export type ToolCallResult = { export type McpRequestContext = { client: ImageKit; stainlessApiKey?: string | undefined; + upstreamClientEnvs?: Record | undefined; }; export type HandlerFunction = ({ From aae29609043cb7d6fc385cfa886e877437397357 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 20:46:44 +0000 Subject: [PATCH 48/50] chore(internal): tweak CI branches --- .github/workflows/ci.yml | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9e900aa1..810d34a6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,12 +1,14 @@ name: CI on: push: - branches-ignore: - - 'generated' - - 'codegen/**' - - 'integrated/**' - - 'stl-preview-head/**' - - 'stl-preview-base/**' + branches: + - '**' + - '!integrated/**' + - '!stl-preview-head/**' + - '!stl-preview-base/**' + - '!generated' + - '!codegen/**' + - 'codegen/stl/**' pull_request: branches-ignore: - 'stl-preview-head/**' From 6e75dad258809bb73b8d4e05195eea89afa7edf8 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 22:00:50 +0000 Subject: [PATCH 49/50] chore(internal): support x-stainless-mcp-client-permissions headers in MCP servers --- packages/mcp-server/src/http.ts | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/packages/mcp-server/src/http.ts b/packages/mcp-server/src/http.ts index d6e14200..e5e53b8b 100644 --- a/packages/mcp-server/src/http.ts +++ b/packages/mcp-server/src/http.ts @@ -40,9 +40,37 @@ const newServer = async ({ } } + // Parse x-stainless-mcp-client-permissions header to override permission options + // + // Note: Permissions are best-effort and intended to prevent clients from doing unexpected things; + // they're not a hard security boundary, so we allow arbitrary, client-driven overrides. + // + // See the Stainless MCP documentation for more details. + let effectiveMcpOptions = mcpOptions; + const clientPermissionsHeader = req.headers['x-stainless-mcp-client-permissions']; + if (typeof clientPermissionsHeader === 'string') { + try { + const parsed = JSON.parse(clientPermissionsHeader); + if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { + effectiveMcpOptions = { + ...mcpOptions, + ...(typeof parsed.allow_http_gets === 'boolean' && { codeAllowHttpGets: parsed.allow_http_gets }), + ...(Array.isArray(parsed.allowed_methods) && { codeAllowedMethods: parsed.allowed_methods }), + ...(Array.isArray(parsed.blocked_methods) && { codeBlockedMethods: parsed.blocked_methods }), + }; + getLogger().info( + { clientPermissions: parsed }, + 'Overriding code execution permissions from x-stainless-mcp-client-permissions header', + ); + } + } catch (error) { + getLogger().warn({ error }, 'Failed to parse x-stainless-mcp-client-permissions header'); + } + } + await initMcpServer({ server: server, - mcpOptions: mcpOptions, + mcpOptions: effectiveMcpOptions, clientOptions: { ...clientOptions, ...authOptions, From e34b820e8ec370598e1b1a0bb110e971adb8a3fe Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 22:01:21 +0000 Subject: [PATCH 50/50] release: 7.4.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 64 +++++++++++++++++++++++++++++++ package.json | 2 +- packages/mcp-server/manifest.json | 10 +++-- packages/mcp-server/package.json | 2 +- packages/mcp-server/src/server.ts | 2 +- src/version.ts | 2 +- 7 files changed, 76 insertions(+), 8 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index c575a162..6731678f 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "7.3.0" + ".": "7.4.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 32b4dc54..1721b382 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,69 @@ # Changelog +## 7.4.0 (2026-03-16) + +Full Changelog: [v7.3.0...v7.4.0](https://github.com/imagekit-developer/imagekit-nodejs/compare/v7.3.0...v7.4.0) + +### Features + +* **api:** dpr type update ([81ec737](https://github.com/imagekit-developer/imagekit-nodejs/commit/81ec737f908203323d163470fd3a86abbb79db27)) +* **api:** revert dpr breaking change ([2543278](https://github.com/imagekit-developer/imagekit-nodejs/commit/2543278ccc78081ffa4cbd82dd1061310c480529)) +* **mcp:** add an option to disable code tool ([af87a8b](https://github.com/imagekit-developer/imagekit-nodejs/commit/af87a8ba4f06ca786ec611177663e21f85e2f8d4)) +* **mcp:** add initial server instructions ([cdce131](https://github.com/imagekit-developer/imagekit-nodejs/commit/cdce131dc17fba5469393a285ac536acd74742b2)) + + +### Bug Fixes + +* **client:** avoid memory leak with abort signals ([c08f7c0](https://github.com/imagekit-developer/imagekit-nodejs/commit/c08f7c04267e000d51cfad22ec8337e456d20171)) +* **client:** avoid removing abort listener too early ([0738e88](https://github.com/imagekit-developer/imagekit-nodejs/commit/0738e8884a59ddac579fab6a65e0221fdff4247c)) +* **client:** preserve URL params already embedded in path ([9fed3c5](https://github.com/imagekit-developer/imagekit-nodejs/commit/9fed3c543ed9c115c50f56d22a52bff307cc6d08)) +* **docs/contributing:** correct pnpm link command ([743a112](https://github.com/imagekit-developer/imagekit-nodejs/commit/743a1126b13d90832ccac545474eda7a1094043f)) +* fix request delays for retrying to be more respectful of high requested delays ([8cc484f](https://github.com/imagekit-developer/imagekit-nodejs/commit/8cc484fda37d8a3188f10f3dfa1cd9ab291f10f6)) +* **mcp:** initialize SDK lazily to avoid failing the connection on init errors ([66c5305](https://github.com/imagekit-developer/imagekit-nodejs/commit/66c53054ce63794350a9939b8f38ecea3ddec428)) +* **mcp:** update prompt ([db925be](https://github.com/imagekit-developer/imagekit-nodejs/commit/db925be3e3cae3ab8cb5e4f4b8f34909a21791e7)) + + +### Chores + +* **ci:** skip uploading artifacts on stainless-internal branches ([7d86a36](https://github.com/imagekit-developer/imagekit-nodejs/commit/7d86a36233f5ea27342329f6545d2b46eddbd219)) +* **client:** do not parse responses with empty content-length ([4b5fcbf](https://github.com/imagekit-developer/imagekit-nodejs/commit/4b5fcbfd1188573ccd1cea40b8e4924a5e2051dc)) +* **client:** restructure abort controller binding ([46c04e1](https://github.com/imagekit-developer/imagekit-nodejs/commit/46c04e16c46bca7bc1b0383d151f027d7d918611)) +* **internal/client:** fix form-urlencoded requests ([d96f483](https://github.com/imagekit-developer/imagekit-nodejs/commit/d96f4832db9e8c16cbeae32f9a7eb46234bb64ed)) +* **internal:** add health check to MCP server when running in HTTP mode ([83d1174](https://github.com/imagekit-developer/imagekit-nodejs/commit/83d1174751241a66748b9d0f4b2b92f37715d4ad)) +* **internal:** allow basic filtering of methods allowed for MCP code mode ([4a86182](https://github.com/imagekit-developer/imagekit-nodejs/commit/4a861827d463d2b6e9812a4aa58d2df14cb356bf)) +* **internal:** allow setting x-stainless-api-key header on mcp server requests ([a72133c](https://github.com/imagekit-developer/imagekit-nodejs/commit/a72133c81d0f9ad9587793bb92c06963fce21e8e)) +* **internal:** always generate MCP server dockerfiles and upgrade associated dependencies ([90eae18](https://github.com/imagekit-developer/imagekit-nodejs/commit/90eae18e29708d7596a6e783cad196c9a4f75f39)) +* **internal:** avoid type checking errors with ts-reset ([7cd3980](https://github.com/imagekit-developer/imagekit-nodejs/commit/7cd398067ad0736b67bfb3d8ace58d15a94c1fd2)) +* **internal:** bump @modelcontextprotocol/sdk, @hono/node-server, and minimatch ([a0031e0](https://github.com/imagekit-developer/imagekit-nodejs/commit/a0031e0a7be090912d18950ae5acffcef7b416e5)) +* **internal:** cache fetch instruction calls in MCP server ([7738ab8](https://github.com/imagekit-developer/imagekit-nodejs/commit/7738ab86d47fbca9a3c05c2cd48910d43d557c43)) +* **internal:** codegen related update ([4178900](https://github.com/imagekit-developer/imagekit-nodejs/commit/41789006a0bdd72bf302bea9c493efcca5927f5d)) +* **internal:** fix MCP Dockerfiles so they can be built without buildkit ([9dde351](https://github.com/imagekit-developer/imagekit-nodejs/commit/9dde351b748d8a2e2fe56321b477ae214e679431)) +* **internal:** fix MCP Dockerfiles so they can be built without buildkit ([62b9ad0](https://github.com/imagekit-developer/imagekit-nodejs/commit/62b9ad0590c40c17cfcd1e7a68f2d5b2b6171cd1)) +* **internal:** improve layout of generated MCP server files ([b2e0a75](https://github.com/imagekit-developer/imagekit-nodejs/commit/b2e0a75d79757596569d0277467ccad531d49bdd)) +* **internal:** make generated MCP servers compatible with Cloudflare worker environments ([419e72f](https://github.com/imagekit-developer/imagekit-nodejs/commit/419e72f03c8854a3c6b6d4654ea474c3c926287c)) +* **internal:** make MCP code execution location configurable via a flag ([497c926](https://github.com/imagekit-developer/imagekit-nodejs/commit/497c9269a3438a872c1ddb7a213d34abe90d6f6b)) +* **internal:** move stringifyQuery implementation to internal function ([60f7ea6](https://github.com/imagekit-developer/imagekit-nodejs/commit/60f7ea6ff1946645f7e65ab93b0c77c4db250f83)) +* **internal:** refactor flag parsing for MCP servers and add debug flag ([ff4b97e](https://github.com/imagekit-developer/imagekit-nodejs/commit/ff4b97e40fb46ca0b4f3229074c3f614b045641c)) +* **internal:** remove mock server code ([f1deef8](https://github.com/imagekit-developer/imagekit-nodejs/commit/f1deef8b69950e5917919a9fc619b558a65fe5b7)) +* **internal:** support oauth authorization code flow for MCP servers ([5f6c688](https://github.com/imagekit-developer/imagekit-nodejs/commit/5f6c688f4f41df60d88fce94bc10cfdce4e29d78)) +* **internal:** support x-stainless-mcp-client-envs header in MCP servers ([6b2f378](https://github.com/imagekit-developer/imagekit-nodejs/commit/6b2f37811676e4d1fdc24a175c477959b7aacde1)) +* **internal:** support x-stainless-mcp-client-permissions headers in MCP servers ([6e75dad](https://github.com/imagekit-developer/imagekit-nodejs/commit/6e75dad258809bb73b8d4e05195eea89afa7edf8)) +* **internal:** switch MCP servers to use pino for logging ([f08d300](https://github.com/imagekit-developer/imagekit-nodejs/commit/f08d3006d05cc29718533591605500ff6edd12f7)) +* **internal:** tweak CI branches ([aae2960](https://github.com/imagekit-developer/imagekit-nodejs/commit/aae29609043cb7d6fc385cfa886e877437397357)) +* **internal:** update agents version ([e39e377](https://github.com/imagekit-developer/imagekit-nodejs/commit/e39e3777d9d0ccd225774c2a889aa6b6c81a6402)) +* **internal:** update dependencies to address dependabot vulnerabilities ([c6f8d11](https://github.com/imagekit-developer/imagekit-nodejs/commit/c6f8d113f12ef419981dfb5df68208c079f1970b)) +* **internal:** upgrade @modelcontextprotocol/sdk and hono ([446fc85](https://github.com/imagekit-developer/imagekit-nodejs/commit/446fc85863574d74d94918ee09aff23f4ed373b4)) +* **internal:** upgrade hono ([61a5d88](https://github.com/imagekit-developer/imagekit-nodejs/commit/61a5d8863e4fcb692d187bb0a7b44e1788faf8ee)) +* **internal:** use x-stainless-mcp-client-envs header for MCP remote code tool calls ([d128af7](https://github.com/imagekit-developer/imagekit-nodejs/commit/d128af71272254b0f6fbc52029e941e674b594cf)) +* **mcp-server:** improve instructions ([9afa574](https://github.com/imagekit-developer/imagekit-nodejs/commit/9afa574d4589f6229784ba766e604a0498f506a3)) +* **mcp-server:** return access instructions for 404 without API key ([568dd2a](https://github.com/imagekit-developer/imagekit-nodejs/commit/568dd2ae4a631c3b64812d1b13af5e6aaba6ebd1)) +* **mcp:** correctly update version in sync with sdk ([57fa966](https://github.com/imagekit-developer/imagekit-nodejs/commit/57fa966d57b1069c554658921a3d649a563e4e12)) +* **mcp:** forward STAINLESS_API_KEY to docs search endpoint ([bf91d73](https://github.com/imagekit-developer/imagekit-nodejs/commit/bf91d7300f134a716038b00bbdcf0cd5932176ea)) +* **tests:** update webhook tests ([f179fa9](https://github.com/imagekit-developer/imagekit-nodejs/commit/f179fa9536d097bcaed31310deeadbfc74457814)) +* update mock server docs ([8d9ad04](https://github.com/imagekit-developer/imagekit-nodejs/commit/8d9ad04f864ca543a065a99cc041a436dd53069a)) +* update placeholder string ([f8f62ae](https://github.com/imagekit-developer/imagekit-nodejs/commit/f8f62ae1890416cb5322c7d7b7ee2235b252983b)) +* use proper capitalization for WebSockets ([e6e8ed6](https://github.com/imagekit-developer/imagekit-nodejs/commit/e6e8ed66ee0b76db104b7112c1540fa021b005ea)) + ## 7.3.0 (2026-02-02) Full Changelog: [v7.2.2...v7.3.0](https://github.com/imagekit-developer/imagekit-nodejs/compare/v7.2.2...v7.3.0) diff --git a/package.json b/package.json index 69664751..af9466f2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@imagekit/nodejs", - "version": "7.3.0", + "version": "7.4.0", "description": "Offical NodeJS SDK for ImageKit.io integration", "author": "Image Kit ", "types": "dist/index.d.ts", diff --git a/packages/mcp-server/manifest.json b/packages/mcp-server/manifest.json index 0e5a8790..934e4661 100644 --- a/packages/mcp-server/manifest.json +++ b/packages/mcp-server/manifest.json @@ -1,7 +1,7 @@ { "dxt_version": "0.2", "name": "@imagekit/api-mcp", - "version": "0.0.1-alpha.0", + "version": "7.4.0", "description": "The official MCP Server for the Image Kit API", "author": { "name": "Image Kit", @@ -18,7 +18,9 @@ "entry_point": "index.js", "mcp_config": { "command": "node", - "args": ["${__dirname}/index.js"], + "args": [ + "${__dirname}/index.js" + ], "env": { "IMAGEKIT_PRIVATE_KEY": "${user_config.IMAGEKIT_PRIVATE_KEY}", "OPTIONAL_IMAGEKIT_IGNORES_THIS": "${user_config.OPTIONAL_IMAGEKIT_IGNORES_THIS}", @@ -53,5 +55,7 @@ "node": ">=18.0.0" } }, - "keywords": ["api"] + "keywords": [ + "api" + ] } diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json index 66ee93da..0b3c2a57 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -1,6 +1,6 @@ { "name": "@imagekit/api-mcp", - "version": "7.3.0", + "version": "7.4.0", "description": "The official MCP Server for the Image Kit API", "author": "Image Kit ", "types": "dist/index.d.ts", diff --git a/packages/mcp-server/src/server.ts b/packages/mcp-server/src/server.ts index 1bfb7083..91ea29b7 100644 --- a/packages/mcp-server/src/server.ts +++ b/packages/mcp-server/src/server.ts @@ -20,7 +20,7 @@ export const newMcpServer = async (stainlessApiKey: string | undefined) => new McpServer( { name: 'imagekit_nodejs_api', - version: '7.3.0', + version: '7.4.0', }, { instructions: await getInstructions(stainlessApiKey), diff --git a/src/version.ts b/src/version.ts index 3f5548b7..06a61128 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const VERSION = '7.3.0'; // x-release-please-version +export const VERSION = '7.4.0'; // x-release-please-version