Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "7.3.0"
".": "7.4.0"
}
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
# Changelog

## 7.4.0 (2026-02-05)

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))


### 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:** refactor flag parsing for MCP servers and add debug flag ([ff4b97e](https://github.com/imagekit-developer/imagekit-nodejs/commit/ff4b97e40fb46ca0b4f3229074c3f614b045641c))
* **internal:** support oauth authorization code flow for MCP servers ([5f6c688](https://github.com/imagekit-developer/imagekit-nodejs/commit/5f6c688f4f41df60d88fce94bc10cfdce4e29d78))

## 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)
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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 <developer@imagekit.io>",
"types": "dist/index.d.ts",
Expand Down
7 changes: 6 additions & 1 deletion packages/mcp-server/package.json
Original file line number Diff line number Diff line change
@@ -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 <developer@imagekit.io>",
"types": "dist/index.d.ts",
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand Down
4 changes: 3 additions & 1 deletion packages/mcp-server/src/headers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { IncomingMessage } from 'node:http';
import { ClientOptions } from '@imagekit/nodejs';

export const parseAuthHeaders = (req: IncomingMessage): Partial<ClientOptions> => {
export const parseAuthHeaders = (req: IncomingMessage, required?: boolean): Partial<ClientOptions> => {
if (req.headers.authorization) {
const scheme = req.headers.authorization.split(' ')[0]!;
const value = req.headers.authorization.slice(scheme.length + 1);
Expand All @@ -19,6 +19,8 @@ export const parseAuthHeaders = (req: IncomingMessage): Partial<ClientOptions> =
'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 =
Expand Down
44 changes: 31 additions & 13 deletions packages/mcp-server/src/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,27 @@

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';

const newServer = ({
const newServer = async ({
clientOptions,
req,
res,
}: {
clientOptions: ClientOptions;
req: express.Request;
res: express.Response;
}): McpServer | null => {
const server = newMcpServer();
}): Promise<McpServer | null> => {
const server = await newMcpServer();

try {
const authOptions = parseAuthHeaders(req);
initMcpServer({
const authOptions = parseAuthHeaders(req, false);
await initMcpServer({
server: server,
clientOptions: {
...clientOptions,
Expand All @@ -45,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();
Expand Down Expand Up @@ -75,32 +76,49 @@ 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('/', get);
app.post('/', post({ clientOptions, mcpOptions }));
app.delete('/', del);

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') {
console.error(`MCP Server running on streamable HTTP at ${address}`);
} 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}`);
}
};
6 changes: 5 additions & 1 deletion packages/mcp-server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Expand Down
27 changes: 15 additions & 12 deletions packages/mcp-server/src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -15,32 +16,33 @@ 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',
choices: ['stdio', 'http'],
default: 'stdio',
description: 'What transport to use; stdio for local servers or http for remote servers',
})
.option('port', {
type: 'number',
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();
Expand All @@ -56,6 +58,7 @@ export function parseCLIOptions(): CLIOptions {

return {
...(includeDocsTools !== undefined && { includeDocsTools }),
debug: !!argv.debug,
transport,
port: argv.port,
socket: argv.socket,
Expand Down
50 changes: 43 additions & 7 deletions packages/mcp-server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
version: '7.4.0',
},
{
instructions: await getInstructions(),
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;
Expand Down
4 changes: 2 additions & 2 deletions packages/mcp-server/src/stdio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
11 changes: 9 additions & 2 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -599,9 +599,10 @@ export class ImageKit {
controller: AbortController,
): Promise<Response> {
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) ||
Expand Down Expand Up @@ -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;
Expand Down
6 changes: 6 additions & 0 deletions src/internal/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ export async function defaultParseResponse<T>(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;
}
Expand Down
2 changes: 1 addition & 1 deletion src/version.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export const VERSION = '7.3.0'; // x-release-please-version
export const VERSION = '7.4.0'; // x-release-please-version