🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@tinify-dev/mcp

Package Overview
Dependencies
Maintainers
1
Versions
8
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@tinify-dev/mcp - npm Package Compare versions

Comparing version
0.1.4
to
0.1.5
+18
-0
CHANGELOG.md

@@ -10,2 +10,20 @@ # Changelog

## [0.1.5] - 2026-07-26
### Changed
- **Starting without `TINIFY_API_KEY` no longer exits.** The server completes
the MCP handshake and registers its tools; calling one returns the
"create a key" instruction as a tool result. Previously the process exited 1
before the handshake, so MCP clients showed only a failed connection and the
explanation — written to stderr on an already-dead process — never reached
the user. This is the state anyone is in immediately after installing the
Claude Code plugin but before exporting a key. The message is still printed
to stderr at startup for people running the binary directly.
### Added
- `repository`, `homepage`, and `bugs` in `package.json`, so the npm page links
back to the source and to tinify.dev.
## [0.1.0] - 2026-07-24

@@ -12,0 +30,0 @@

+157
-105

@@ -6,3 +6,3 @@ #!/usr/bin/env node

// node_modules/@tinify-dev/client/dist/chunk-NYRKB5BB.js
// node_modules/@tinify-dev/client/dist/chunk-F4JYNVER.js
var DEFAULT_BASE_URL = "https://api.tinify.dev";

@@ -15,2 +15,3 @@ var ENV_API_KEY = "TINIFY_API_KEY";

var BACKOFF_CAP_MS = 8e3;
var MAX_RETRY_AFTER_MS = 3e4;
var MAX_FILE_BYTES = 41943040;

@@ -112,52 +113,2 @@ var DEFAULT_UPLOAD_CONCURRENCY = 4;

}
var TERMINAL = new Set(BATCH_TERMINAL_STATUSES);
function isTerminalBatchStatus(status) {
return TERMINAL.has(status);
}
async function uploadBatchFiles(fetchImpl, session, files, options = {}) {
const missing = session.uploads.map((upload) => upload.client_id).filter((clientId) => !(clientId in files));
if (missing.length > 0) {
throw new TinifyError(
`Missing input for batch client_id(s): ${missing.join(", ")}. Pass a { client_id: input } record covering every file in the session.`
);
}
const queue = [...session.uploads];
const concurrency = Math.max(
1,
Math.min(options.concurrency ?? DEFAULT_UPLOAD_CONCURRENCY, queue.length)
);
const workers = Array.from({ length: concurrency }, async () => {
for (; ; ) {
const upload = queue.shift();
if (upload === void 0) return;
options.signal?.throwIfAborted();
await putOne(fetchImpl, upload, files[upload.client_id], options.signal);
}
});
await Promise.all(workers);
}
async function putOne(fetchImpl, upload, input, signal) {
const { blob } = await normalizeInput(input);
let response;
try {
response = await fetchImpl(upload.upload_url, {
method: "PUT",
headers: upload.headers,
body: blob,
...signal !== void 0 ? { signal } : {}
});
} catch (error) {
throw new TinifyNetworkError(
`Upload for client_id "${upload.client_id}" failed: ${error instanceof Error ? error.message : String(error)}`,
{ cause: error }
);
}
if (!response.ok) {
const text = await response.text().catch(() => "");
throw new TinifyError(
`Upload for client_id "${upload.client_id}" was rejected with HTTP ${response.status}` + (text ? `: ${text.slice(0, 200)}` : ".")
);
}
await response.arrayBuffer().catch(() => void 0);
}
var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 503]);

@@ -232,2 +183,34 @@ function generateIdempotencyKey() {

}
function withTimeoutSignal(timeoutMs, signal) {
const timeout = AbortSignal.timeout(timeoutMs);
if (signal === void 0) return { signal: timeout, timeout };
const anyOf = AbortSignal.any;
if (typeof anyOf === "function") return { signal: anyOf([signal, timeout]), timeout };
const controller = new AbortController();
const abortFrom = (src) => controller.abort(src.reason);
if (signal.aborted) controller.abort(signal.reason);
else if (timeout.aborted) controller.abort(timeout.reason);
else {
signal.addEventListener("abort", () => abortFrom(signal), { once: true });
timeout.addEventListener("abort", () => abortFrom(timeout), { once: true });
}
return { signal: controller.signal, timeout };
}
function asTimeoutError(error, timeoutMs) {
if (typeof error === "object" && error !== null && error.name === "TimeoutError") {
return new TinifyTimeoutError(`Request timed out after ${timeoutMs} ms.`, timeoutMs);
}
return error;
}
async function fetchWithTimeout(fetchImpl, url, init, timeoutMs, userSignal) {
const { signal, timeout } = withTimeoutSignal(timeoutMs, userSignal);
try {
return await fetchImpl(url, { ...init, signal });
} catch (error) {
if (timeout.aborted) {
throw new TinifyTimeoutError(`Request timed out after ${timeoutMs} ms.`, timeoutMs);
}
throw error;
}
}
var HttpClient = class {

@@ -265,3 +248,3 @@ config;

accept: "application/json",
"user-agent": "@tinify-dev/client/0.1.0"
"user-agent": "@tinify-dev/client/0.1.1"
};

@@ -272,3 +255,4 @@ if (idempotencyKey !== void 0) headers["idempotency-key"] = idempotencyKey;

try {
response = await this.fetchWithTimeout(
response = await fetchWithTimeout(
this.config.fetchImpl,
url,

@@ -280,2 +264,3 @@ {

},
this.config.timeoutMs,
options.signal

@@ -299,3 +284,4 @@ );

await response.arrayBuffer().catch(() => void 0);
await sleep(retryAfter ?? backoffMs(attempt), options.signal);
const wait = Math.min(retryAfter ?? backoffMs(attempt), MAX_RETRY_AFTER_MS);
await sleep(wait, options.signal);
attempt += 1;

@@ -316,6 +302,10 @@ continue;

let body;
if (result.response.status === 204) {
body = void 0;
} else {
body = await result.response.json();
try {
if (result.response.status === 204) {
body = void 0;
} else {
body = await result.response.json();
}
} catch (error) {
throw asTimeoutError(error, this.config.timeoutMs);
}

@@ -329,25 +319,59 @@ const bodyRequestId = body?.request_id;

}
async fetchWithTimeout(url, init, signal) {
const controller = new AbortController();
const timeoutError = new TinifyTimeoutError(
`Request timed out after ${this.config.timeoutMs} ms.`,
this.config.timeoutMs
/** The configured per-request timeout (ms); used by download/upload paths. */
get timeoutMs() {
return this.config.timeoutMs;
}
};
var TERMINAL = new Set(BATCH_TERMINAL_STATUSES);
function isTerminalBatchStatus(status) {
return TERMINAL.has(status);
}
async function uploadBatchFiles(fetchImpl, session, files, options, timeoutMs) {
const missing = session.uploads.map((upload) => upload.client_id).filter((clientId) => !(clientId in files));
if (missing.length > 0) {
throw new TinifyError(
`Missing input for batch client_id(s): ${missing.join(", ")}. Pass a { client_id: input } record covering every file in the session.`
);
const timer = setTimeout(() => controller.abort(timeoutError), this.config.timeoutMs);
const onAbort = () => controller.abort(abortReason(signal));
signal?.addEventListener("abort", onAbort, { once: true });
try {
return await this.config.fetchImpl(url, { ...init, signal: controller.signal });
} catch (error) {
if (error === timeoutError) throw error;
if (controller.signal.aborted && controller.signal.reason === timeoutError) {
throw timeoutError;
}
throw error;
} finally {
clearTimeout(timer);
signal?.removeEventListener("abort", onAbort);
}
const queue = [...session.uploads];
const concurrency = Math.max(
1,
Math.min(options.concurrency ?? DEFAULT_UPLOAD_CONCURRENCY, queue.length)
);
const workers = Array.from({ length: concurrency }, async () => {
for (; ; ) {
const upload = queue.shift();
if (upload === void 0) return;
options.signal?.throwIfAborted();
await putOne(fetchImpl, upload, files[upload.client_id], timeoutMs, options.signal);
}
});
await Promise.all(workers);
}
async function putOne(fetchImpl, upload, input, timeoutMs, signal) {
const { blob } = await normalizeInput(input);
let response;
try {
response = await fetchWithTimeout(
fetchImpl,
upload.upload_url,
{ method: "PUT", headers: upload.headers, body: blob },
timeoutMs,
signal
);
} catch (error) {
if (error instanceof TinifyTimeoutError) throw error;
throw new TinifyNetworkError(
`Upload for client_id "${upload.client_id}" failed: ${error instanceof Error ? error.message : String(error)}`,
{ cause: error }
);
}
};
if (!response.ok) {
const text = await response.text().catch(() => "");
throw new TinifyError(
`Upload for client_id "${upload.client_id}" was rejected with HTTP ${response.status}` + (text ? `: ${text.slice(0, 200)}` : ".")
);
}
await response.arrayBuffer().catch(() => void 0);
}
var TinifyClient = class {

@@ -454,8 +478,15 @@ http;

const url = new URL(raw, `${this.http.baseUrl}/`).toString();
const response = await this.http.fetchImpl(url, {
method: "GET",
...options.signal !== void 0 ? { signal: options.signal } : {}
});
const response = await fetchWithTimeout(
this.http.fetchImpl,
url,
{ method: "GET" },
this.http.timeoutMs,
options.signal
);
if (!response.ok) await throwApiError(response);
return response.blob();
try {
return await response.blob();
} catch (error) {
throw asTimeoutError(error, this.http.timeoutMs);
}
}

@@ -485,3 +516,3 @@ // -------------------------------------------------------------------------

uploadBatchFiles(session, files, options = {}) {
return uploadBatchFiles(this.http.fetchImpl, session, files, options);
return uploadBatchFiles(this.http.fetchImpl, session, files, options, this.http.timeoutMs);
}

@@ -551,3 +582,7 @@ /** Commits an uploaded batch for processing. */

});
return result.response.blob();
try {
return await result.response.blob();
} catch (error) {
throw asTimeoutError(error, this.http.timeoutMs);
}
}

@@ -579,6 +614,6 @@ // -------------------------------------------------------------------------

// src/config.ts
var ConfigError = class extends Error {
var MissingApiKeyError = class extends Error {
constructor(message) {
super(message);
this.name = "ConfigError";
this.name = "MissingApiKeyError";
}

@@ -605,9 +640,10 @@ };

].join("\n");
function loadConfig(env) {
var missingApiKeyMessage = MISSING_KEY_MESSAGE;
function readConfig(env) {
const apiKey = env["TINIFY_API_KEY"];
if (apiKey === void 0 || apiKey.trim() === "") {
throw new ConfigError(MISSING_KEY_MESSAGE);
}
const baseUrl = env["TINIFY_BASE_URL"];
return baseUrl !== void 0 && baseUrl !== "" ? { apiKey, baseUrl } : { apiKey };
return {
...apiKey !== void 0 && apiKey.trim() !== "" ? { apiKey } : {},
...baseUrl !== void 0 && baseUrl !== "" ? { baseUrl } : {}
};
}

@@ -720,2 +756,5 @@

function toErrorResult(error) {
if (error instanceof MissingApiKeyError) {
return errorResult(error.message);
}
if (error instanceof ToolInputError) {

@@ -1103,3 +1142,3 @@ return errorResult(error.message);

var SERVER_NAME = "tinify";
var SERVER_VERSION = "0.1.4";
var SERVER_VERSION = "0.1.5";
var SERVER_TITLE = "Tinify image tools";

@@ -1145,15 +1184,21 @@ var SERVER_DESCRIPTION = "Compress, resize, crop, and convert images with the Tinify.dev API. Honest results: never returns a larger file.";

// src/unconfigured-client.ts
function createUnconfiguredClient() {
const refuse = () => {
throw new MissingApiKeyError(missingApiKeyMessage);
};
return {
compress: refuse,
resize: refuse,
crop: refuse,
convert: refuse,
usage: refuse,
download: refuse
};
}
// src/index.ts
async function main() {
let config;
try {
config = loadConfig(process.env);
} catch (error) {
if (error instanceof ConfigError) {
console.error(error.message);
process.exit(1);
}
throw error;
}
const client = new TinifyClient({
const config = readConfig(process.env);
const client = config.apiKey === void 0 ? createUnconfiguredClient() : new TinifyClient({
apiKey: config.apiKey,

@@ -1165,3 +1210,10 @@ ...config.baseUrl !== void 0 ? { baseUrl: config.baseUrl } : {}

await server.connect(transport);
console.error("tinify-mcp: ready on stdio (5 tools registered)");
if (config.apiKey === void 0) {
console.error(missingApiKeyMessage);
console.error(
"\ntinify-mcp: started without a key - tools are listed but will refuse until TINIFY_API_KEY is set."
);
} else {
console.error("tinify-mcp: ready on stdio (5 tools registered)");
}
}

@@ -1168,0 +1220,0 @@ main().catch((error) => {

{
"name": "@tinify-dev/mcp",
"version": "0.1.4",
"version": "0.1.5",
"description": "MCP server for the Tinify.dev image API: compress, resize, crop, and convert images from Claude, Cursor, or any MCP client.",
"license": "MIT",
"author": "Stian Larsen",
"homepage": "https://tinify.dev/mcp",
"repository": {
"type": "git",
"url": "git+https://github.com/Stianlars1/tinify-mcp.git"
},
"bugs": {
"url": "https://github.com/Stianlars1/tinify-mcp/issues"
},
"type": "module",

@@ -44,3 +52,3 @@ "sideEffects": false,

"@modelcontextprotocol/sdk": "^1.29.0",
"@tinify-dev/client": "^0.1.0",
"@tinify-dev/client": "^0.1.1",
"zod": "^3.25.76"

@@ -47,0 +55,0 @@ },

@@ -10,3 +10,3 @@ {

},
"version": "0.1.4",
"version": "0.1.5",
"websiteUrl": "https://tinify.dev/mcp",

@@ -25,3 +25,3 @@ "icons": [

"identifier": "@tinify-dev/mcp",
"version": "0.1.4",
"version": "0.1.5",
"transport": {

@@ -28,0 +28,0 @@ "type": "stdio"