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/.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..3afd244b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,40 @@ # Changelog +## 7.4.0 (2026-02-19) + +Full Changelog: [v7.3.0...v7.4.0](https://github.com/imagekit-developer/imagekit-nodejs/compare/v7.3.0...v7.4.0) + +### Features + +* **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)) +* **mcp:** initialize SDK lazily to avoid failing the connection on init errors ([66c5305](https://github.com/imagekit-developer/imagekit-nodejs/commit/66c53054ce63794350a9939b8f38ecea3ddec428)) + + +### Chores + +* **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:** cache fetch instruction calls in MCP server ([7738ab8](https://github.com/imagekit-developer/imagekit-nodejs/commit/7738ab86d47fbca9a3c05c2cd48910d43d557c43)) +* **internal:** improve layout of generated MCP server files ([b2e0a75](https://github.com/imagekit-developer/imagekit-nodejs/commit/b2e0a75d79757596569d0277467ccad531d49bdd)) +* **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:** upgrade hono ([61a5d88](https://github.com/imagekit-developer/imagekit-nodejs/commit/61a5d8863e4fcb692d187bb0a7b44e1788faf8ee)) +* **mcp:** forward STAINLESS_API_KEY to docs search endpoint ([bf91d73](https://github.com/imagekit-developer/imagekit-nodejs/commit/bf91d7300f134a716038b00bbdcf0cd5932176ea)) +* update mock server docs ([8d9ad04](https://github.com/imagekit-developer/imagekit-nodejs/commit/8d9ad04f864ca543a065a99cc041a436dd53069a)) + ## 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/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/package.json b/package.json index 2c422387..44bfd22b 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/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 [] 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" } diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json index 56c2098f..076a5995 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", @@ -34,10 +34,13 @@ "@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", + "morgan-body": "^2.6.9", "qs": "^6.14.1", "typescript": "5.8.3", "yargs": "^17.7.2", @@ -50,9 +53,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/auth.ts similarity index 59% rename from packages/mcp-server/src/headers.ts rename to packages/mcp-server/src/auth.ts index 40fd3090..085cac43 100644 --- a/packages/mcp-server/src/headers.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): 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); @@ -19,6 +20,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 = @@ -31,3 +34,17 @@ export const parseAuthHeaders = (req: IncomingMessage): Partial = : 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 220b5c74..932583e7 100644 --- a/packages/mcp-server/src/code-tool.ts +++ b/packages/mcp-server/src/code-tool.ts @@ -1,10 +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 './server'; +import { readEnv, requireValue } from './util'; import { WorkerInput, WorkerOutput } from './code-tool-types'; -import { ImageKit } from '@imagekit/nodejs'; +import { SdkMethod } from './methods'; const prompt = `Runs JavaScript code to interact with the Image Kit API. @@ -35,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(): McpTool { +export function codeTool({ blockedMethods }: { blockedMethods: SdkMethod[] | undefined }): McpTool { const metadata: Metadata = { resource: 'all', operation: 'write', tags: [] }; const tool: Tool = { name: 'execute', @@ -55,20 +62,41 @@ export function codeTool(): McpTool { 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 (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 + .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'); 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 9af38ab5..6d78bb6b 100644 --- a/packages/mcp-server/src/docs-search-tool.ts +++ b/packages/mcp-server/src/docs-search-tool.ts @@ -1,7 +1,6 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { Metadata, asTextContentResult } from './types'; - +import { Metadata, McpRequestContext, asTextContentResult } from './types'; import { Tool } from '@modelcontextprotocol/sdk/types.js'; export const metadata: Metadata = { @@ -42,10 +41,20 @@ 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 result = await fetch(`${docsSearchURL}?${query}`); + const result = await fetch(`${docsSearchURL}?${query}`, { + headers: { + ...(reqContext.stainlessApiKey && { Authorization: reqContext.stainlessApiKey }), + }, + }); if (!result.ok) { throw new Error( diff --git a/packages/mcp-server/src/http.ts b/packages/mcp-server/src/http.ts index dcfeba6a..f3354511 100644 --- a/packages/mcp-server/src/http.ts +++ b/packages/mcp-server/src/http.ts @@ -2,42 +2,39 @@ 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 { getStainlessApiKey, parseClientAuthHeaders } from './auth'; import { McpOptions } from './options'; -import { ClientOptions, initMcpServer, newMcpServer } from './server'; -import { parseAuthHeaders } from './headers'; +import { initMcpServer, newMcpServer } from './server'; -const newServer = ({ +const newServer = async ({ clientOptions, + mcpOptions, req, res, }: { clientOptions: ClientOptions; + mcpOptions: McpOptions; req: express.Request; res: express.Response; -}): McpServer | null => { - const server = newMcpServer(); +}): Promise => { + const stainlessApiKey = getStainlessApiKey(req, mcpOptions); + const server = await newMcpServer(stainlessApiKey); - try { - const authOptions = parseAuthHeaders(req); - initMcpServer({ - server: server, - clientOptions: { - ...clientOptions, - ...authOptions, - }, - }); - } catch (error) { - res.status(401).json({ - jsonrpc: '2.0', - error: { - code: -32000, - message: `Unauthorized: ${error instanceof Error ? error.message : error}`, - }, - }); - return null; - } + const authOptions = parseClientAuthHeaders(req, false); + + await initMcpServer({ + server: server, + mcpOptions: mcpOptions, + clientOptions: { + ...clientOptions, + ...authOptions, + }, + stainlessApiKey: stainlessApiKey, + }); return server; }; @@ -45,7 +42,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(); @@ -75,15 +72,31 @@ const del = async (req: express.Request, res: express.Response) => { export const streamableHTTPApp = ({ clientOptions = {}, - mcpOptions = {}, + mcpOptions, + debug, }: { clientOptions?: ClientOptions; - mcpOptions?: McpOptions; + 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.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); @@ -91,8 +104,16 @@ export const streamableHTTPApp = ({ return app; }; -export const launchStreamableHTTPServer = async (options: McpOptions, port: number | string | undefined) => { - const app = streamableHTTPApp({ mcpOptions: options }); +export const launchStreamableHTTPServer = async ({ + mcpOptions, + debug, + port, +}: { + mcpOptions: McpOptions; + debug: boolean; + port: number | string | undefined; +}) => { + const app = streamableHTTPApp({ mcpOptions, debug }); const server = app.listen(port); const address = server.address(); diff --git a/packages/mcp-server/src/index.ts b/packages/mcp-server/src/index.ts index 0f6dd426..654d25cf 100644 --- a/packages/mcp-server/src/index.ts +++ b/packages/mcp-server/src/index.ts @@ -18,10 +18,14 @@ async function main() { switch (options.transport) { case 'stdio': - await launchStdioServer(); + await launchStdioServer(options); break; case 'http': - await launchStreamableHTTPServer(options, options.port ?? options.socket); + await launchStreamableHTTPServer({ + mcpOptions: options, + debug: options.debug, + port: options.socket ?? options.port, + }); break; } } 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/methods.ts b/packages/mcp-server/src/methods.ts new file mode 100644 index 00000000..a2a4f930 --- /dev/null +++ b/packages/mcp-server/src/methods.ts @@ -0,0 +1,370 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +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 c66ad8ce..32a88713 100644 --- a/packages/mcp-server/src/options.ts +++ b/packages/mcp-server/src/options.ts @@ -1,9 +1,13 @@ +// 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'; import z from 'zod'; +import { readEnv } from './util'; export type CLIOptions = McpOptions & { + debug: boolean; transport: 'stdio' | 'http'; port: number | undefined; socket: string | undefined; @@ -11,36 +15,64 @@ export type CLIOptions = McpOptions & { export type McpOptions = { includeDocsTools?: boolean | undefined; + stainlessApiKey?: string | undefined; + codeAllowHttpGets?: boolean | undefined; + codeAllowedMethods?: string[] | undefined; + codeBlockedMethods?: string[] | undefined; }; export function parseCLIOptions(): CLIOptions { const opts = yargs(hideBin(process.argv)) - .option('tools', { + .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, - choices: ['code', 'docs'], - description: 'Use dynamic tools or all tools', + 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('no-tools', { + .option('code-blocked-methods', { type: 'string', array: true, - choices: ['code', 'docs'], - description: 'Do not use any dynamic or all tools', + 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('transport', { + .option('debug', { type: 'boolean', description: 'Enable debug logging' }) + .option('no-tools', { type: 'string', - choices: ['stdio', 'http'], - default: 'stdio', - description: 'What transport to use; stdio for local servers or http for remote servers', + array: true, + choices: ['code', 'docs'], + description: 'Tools to explicitly disable', }) .option('port', { type: 'number', + default: 3000, description: 'Port to serve on if using http transport', }) - .option('socket', { + .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, + choices: ['code', 'docs'], + description: 'Tools to explicitly enable', + }) + .option('transport', { type: 'string', - description: 'Unix socket to serve on if using http transport', + choices: ['stdio', 'http'], + default: 'stdio', + description: 'What transport to use; stdio for local servers or http for remote servers', }) + .env('MCP_SERVER') + .version(true) .help(); const argv = opts.parseSync(); @@ -56,6 +88,11 @@ export function parseCLIOptions(): CLIOptions { return { ...(includeDocsTools !== undefined && { includeDocsTools }), + debug: !!argv.debug, + stainlessApiKey: argv.stainlessApiKey, + 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 7b26259a..7e89fa72 100644 --- a/packages/mcp-server/src/server.ts +++ b/packages/mcp-server/src/server.ts @@ -11,32 +11,32 @@ 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 { HandlerFunction, McpTool } from './types'; +import { blockedMethodsForCodeTool } from './methods'; +import { HandlerFunction, McpRequestContext, ToolCallResult, McpTool } from './types'; -export { McpOptions } from './options'; -export { ClientOptions } from '@imagekit/nodejs'; - -export const newMcpServer = () => +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), + capabilities: { tools: {}, logging: {} }, }, - { 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; + stainlessApiKey?: string | undefined; }) { const server = params.server instanceof McpServer ? params.server.server : params.server; @@ -55,14 +55,32 @@ export 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,29 +98,56 @@ export function initMcpServer(params: { throw new Error(`Unknown tool: ${name}`); } - return executeHandler(mcpTool.handler, client, args); + 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: { + client, + stainlessApiKey: params.stainlessApiKey ?? params.mcpOptions?.stainlessApiKey, + }, + args, + }); }); 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 {}; }); } @@ -111,7 +156,11 @@ export 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); } @@ -121,34 +170,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 || {} }); } - -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/stdio.ts b/packages/mcp-server/src/stdio.ts index f07696f3..ceccaed3 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 () => { - const server = newMcpServer(); +export const launchStdioServer = async (mcpOptions: McpOptions) => { + const server = await newMcpServer(mcpOptions.stainlessApiKey); - initMcpServer({ server }); + 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 { 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; +}; 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 "$@" diff --git a/src/client.ts b/src/client.ts index ef1cc2fb..9b293fb3 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( @@ -599,9 +599,10 @@ export class ImageKit { controller: AbortController, ): Promise { const { signal, method, ...options } = init || {}; - if (signal) signal.addEventListener('abort', () => controller.abort()); + const abort = this._makeAbort(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) || @@ -768,6 +769,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; @@ -800,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 }); } 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; } 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 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();