From 1fb60a5677db5340f77c03b140e9266545f0bbed Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Wed, 8 Oct 2025 18:54:23 +0100 Subject: [PATCH 01/16] Add outputType to zip tool: (inlined /) resource link / resource --- src/everything/everything.ts | 70 +++++++++++++++++++++++++++++------- 1 file changed, 58 insertions(+), 12 deletions(-) diff --git a/src/everything/everything.ts b/src/everything/everything.ts index 5086364dd..bb7f5cd99 100644 --- a/src/everything/everything.ts +++ b/src/everything/everything.ts @@ -132,6 +132,11 @@ const StructuredContentSchema = { const ZipResourcesInputSchema = z.object({ files: z.record(z.string().url().describe("URL of the file to include in the zip")).describe("Mapping of file names to URLs to include in the zip"), + outputType: z.enum([ + 'resourceLink', + 'inlinedResourceLink', + 'resource' + ]).default('inlinedResourceLink').describe("How the resulting zip file should be returned. 'resourceLink' returns a linked to a resource that can be read later, 'inlinedResourceLink' returns a resource_link with a data URI, and 'resource' returns a full resource object."), }); enum ToolName { @@ -334,9 +339,17 @@ export const createServer = () => { }; }); + const transientResources = new Map(); + server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const uri = request.params.uri; + if (transientResources.has(uri)) { + return { + contents: [transientResources.get(uri)!], + }; + } + if (uri.startsWith("test://static/resource/")) { const index = parseInt(uri.split("/").pop() ?? "", 10) - 1; if (index >= 0 && index < ALL_RESOURCES.length) { @@ -859,7 +872,7 @@ export const createServer = () => { } if (name === ToolName.ZIP_RESOURCES) { - const { files } = ZipResourcesInputSchema.parse(args); + const { files, outputType } = ZipResourcesInputSchema.parse(args); const zip = new JSZip(); @@ -876,17 +889,50 @@ export const createServer = () => { } } - const uri = `data:application/zip;base64,${await zip.generateAsync({ type: "base64" })}`; - - return { - content: [ - { - type: "resource_link", - mimeType: "application/zip", - uri, - }, - ], - }; + const blob = await zip.generateAsync({ type: "base64" }); + if (outputType === 'inlinedResourceLink') { + return { + content: [ + { + type: "resource_link", + mimeType: "application/zip", + uri: `data:application/zip;base64,${blob}`, + }, + ], + }; + } else { + const name = `out_${Date.now()}.zip`; + const uri = `resource://${name}`; + const resource = { + uri, + name, + mimeType: "application/zip", + blob, + }; + if (outputType === 'resource') { + return { + content: [ + { + type: "resource", + resource, + }, + ], + }; + } else if (outputType === 'resourceLink') { + transientResources.set(uri, resource); + return { + content: [ + { + type: "resource_link", + mimeType: "application/zip", + uri, + }, + ], + }; + } else { + throw new Error(`Unknown outputType: ${outputType}`); + } + } } if (name === ToolName.LIST_ROOTS) { From e1d2f40f95b54c666d21a81de1d41eef1472ab04 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Wed, 8 Oct 2025 19:03:25 +0100 Subject: [PATCH 02/16] Update everything.ts --- src/everything/everything.ts | 38 ++++++------------------------------ 1 file changed, 6 insertions(+), 32 deletions(-) diff --git a/src/everything/everything.ts b/src/everything/everything.ts index bb7f5cd99..a33c25acb 100644 --- a/src/everything/everything.ts +++ b/src/everything/everything.ts @@ -890,45 +890,19 @@ export const createServer = () => { } const blob = await zip.generateAsync({ type: "base64" }); + const mimeType = "application/zip"; if (outputType === 'inlinedResourceLink') { - return { - content: [ - { - type: "resource_link", - mimeType: "application/zip", - uri: `data:application/zip;base64,${blob}`, - }, - ], - }; + const uri = `data:${mimeType};base64,${blob}`; + return {content: [{type: "resource_link", mimeType, uri}]}; } else { const name = `out_${Date.now()}.zip`; const uri = `resource://${name}`; - const resource = { - uri, - name, - mimeType: "application/zip", - blob, - }; + const resource = {uri, name, mimeType, blob}; if (outputType === 'resource') { - return { - content: [ - { - type: "resource", - resource, - }, - ], - }; + return {content: [{ type: "resource", resource}]}; } else if (outputType === 'resourceLink') { transientResources.set(uri, resource); - return { - content: [ - { - type: "resource_link", - mimeType: "application/zip", - uri, - }, - ], - }; + return {content: [{ type: "resource_link", mimeType, uri }]}; } else { throw new Error(`Unknown outputType: ${outputType}`); } From 893ee9c5b7d34e8cd7d6197e5305ef80297e89a8 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Wed, 8 Oct 2025 19:05:28 +0100 Subject: [PATCH 03/16] Update everything.ts --- src/everything/everything.ts | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/everything/everything.ts b/src/everything/everything.ts index a33c25acb..90d16a67c 100644 --- a/src/everything/everything.ts +++ b/src/everything/everything.ts @@ -893,16 +893,33 @@ export const createServer = () => { const mimeType = "application/zip"; if (outputType === 'inlinedResourceLink') { const uri = `data:${mimeType};base64,${blob}`; - return {content: [{type: "resource_link", mimeType, uri}]}; + return { + content: [{ + type: "resource_link", + mimeType, + uri + }] + }; } else { const name = `out_${Date.now()}.zip`; const uri = `resource://${name}`; const resource = {uri, name, mimeType, blob}; if (outputType === 'resource') { - return {content: [{ type: "resource", resource}]}; + return { + content: [{ + type: "resource", + resource + }] + }; } else if (outputType === 'resourceLink') { transientResources.set(uri, resource); - return {content: [{ type: "resource_link", mimeType, uri }]}; + return { + content: [{ + type: "resource_link", + mimeType, + uri + }] + }; } else { throw new Error(`Unknown outputType: ${outputType}`); } From bd496fe7d850a8dfd07f8d268950a4a579d8918d Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Thu, 9 Oct 2025 16:07:38 +0100 Subject: [PATCH 04/16] remove inlinedResourceLink --- src/everything/everything.ts | 40 +++++++++++++----------------------- 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/src/everything/everything.ts b/src/everything/everything.ts index 90d16a67c..143159ac3 100644 --- a/src/everything/everything.ts +++ b/src/everything/everything.ts @@ -134,9 +134,8 @@ const ZipResourcesInputSchema = z.object({ files: z.record(z.string().url().describe("URL of the file to include in the zip")).describe("Mapping of file names to URLs to include in the zip"), outputType: z.enum([ 'resourceLink', - 'inlinedResourceLink', 'resource' - ]).default('inlinedResourceLink').describe("How the resulting zip file should be returned. 'resourceLink' returns a linked to a resource that can be read later, 'inlinedResourceLink' returns a resource_link with a data URI, and 'resource' returns a full resource object."), + ]).default('resource').describe("How the resulting zip file should be returned. 'resourceLink' returns a linked to a resource that can be read later, 'resource' returns a full resource object."), }); enum ToolName { @@ -891,8 +890,18 @@ export const createServer = () => { const blob = await zip.generateAsync({ type: "base64" }); const mimeType = "application/zip"; - if (outputType === 'inlinedResourceLink') { - const uri = `data:${mimeType};base64,${blob}`; + const name = `out_${Date.now()}.zip`; + const uri = `resource://${name}`; + const resource = {uri, name, mimeType, blob}; + if (outputType === 'resource') { + return { + content: [{ + type: "resource", + resource + }] + }; + } else if (outputType === 'resourceLink') { + transientResources.set(uri, resource); return { content: [{ type: "resource_link", @@ -901,28 +910,7 @@ export const createServer = () => { }] }; } else { - const name = `out_${Date.now()}.zip`; - const uri = `resource://${name}`; - const resource = {uri, name, mimeType, blob}; - if (outputType === 'resource') { - return { - content: [{ - type: "resource", - resource - }] - }; - } else if (outputType === 'resourceLink') { - transientResources.set(uri, resource); - return { - content: [{ - type: "resource_link", - mimeType, - uri - }] - }; - } else { - throw new Error(`Unknown outputType: ${outputType}`); - } + throw new Error(`Unknown outputType: ${outputType}`); } } From ee5f8031350a2b17c23d0070b5fb542c0fae7930 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Thu, 9 Oct 2025 16:13:41 +0100 Subject: [PATCH 05/16] Update everything.ts --- src/everything/everything.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/everything/everything.ts b/src/everything/everything.ts index 143159ac3..c6a246472 100644 --- a/src/everything/everything.ts +++ b/src/everything/everything.ts @@ -872,7 +872,6 @@ export const createServer = () => { if (name === ToolName.ZIP_RESOURCES) { const { files, outputType } = ZipResourcesInputSchema.parse(args); - const zip = new JSZip(); for (const [fileName, fileUrl] of Object.entries(files)) { From 48d8afe4dac14cbe7fe13bb74b02eb277322d093 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Thu, 9 Oct 2025 16:14:55 +0100 Subject: [PATCH 06/16] Forcing an empty commit. From 12e313bf13d5082373a17fea17212561a420eccf Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Thu, 9 Oct 2025 16:15:42 +0100 Subject: [PATCH 07/16] Forcing an empty commit. From f3c5e4f312dc16969e491a1877640212e8b87416 Mon Sep 17 00:00:00 2001 From: Cliff Hall Date: Fri, 10 Oct 2025 10:55:27 -0400 Subject: [PATCH 08/16] Apply suggestion from @cliffhall --- src/everything/everything.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/everything/everything.ts b/src/everything/everything.ts index c6a246472..ade7ef68d 100644 --- a/src/everything/everything.ts +++ b/src/everything/everything.ts @@ -135,7 +135,7 @@ const ZipResourcesInputSchema = z.object({ outputType: z.enum([ 'resourceLink', 'resource' - ]).default('resource').describe("How the resulting zip file should be returned. 'resourceLink' returns a linked to a resource that can be read later, 'resource' returns a full resource object."), + ]).default('resource').describe("How the resulting zip file should be returned. 'resourceLink' returns a link to a resource that can be read later, 'resource' returns a full resource object."), }); enum ToolName { From 0ee497db3eb3a1f48c847186a17d87f00115466d Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Tue, 14 Oct 2025 17:48:11 +0100 Subject: [PATCH 09/16] only accept http(s) and data uris in zip tool --- src/everything/everything.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/everything/everything.ts b/src/everything/everything.ts index ade7ef68d..f7ce1869a 100644 --- a/src/everything/everything.ts +++ b/src/everything/everything.ts @@ -874,8 +874,12 @@ export const createServer = () => { const { files, outputType } = ZipResourcesInputSchema.parse(args); const zip = new JSZip(); - for (const [fileName, fileUrl] of Object.entries(files)) { + for (const [fileName, fileUrlString] of Object.entries(files)) { try { + const fileUrl = new URL(fileUrlString); + if (fileUrl.protocol !== 'http:' && fileUrl.protocol !== 'https:' && fileUrl.protocol !== 'data:') { + throw new Error(`Unsupported URL protocol for ${fileUrlString}. Only http, https, and data URLs are supported.`); + } const response = await fetch(fileUrl); if (!response.ok) { throw new Error(`Failed to fetch ${fileUrl}: ${response.statusText}`); From 382a83af5644b9aafdfb2979dc52abbf00e1eda2 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Tue, 14 Oct 2025 20:05:23 +0100 Subject: [PATCH 10/16] added fetchSafely (maxBytes + timeout), MAX_ZIP_FETCH_SIZE, MAX_ZIP_FETCH_TIME_MILLIS --- src/everything/everything.ts | 86 +++++++++++++++++++++++++++++++++--- 1 file changed, 79 insertions(+), 7 deletions(-) diff --git a/src/everything/everything.ts b/src/everything/everything.ts index ae469a10d..1c55221af 100644 --- a/src/everything/everything.ts +++ b/src/everything/everything.ts @@ -868,23 +868,37 @@ export const createServer = () => { } if (name === ToolName.ZIP_RESOURCES) { + const MAX_ZIP_FETCH_SIZE = Number(process.env.MAX_ZIP_FETCH_SIZE ?? String(10 * 1024 * 1024)); // 10 MB default + const MAX_ZIP_FETCH_TIME_MILLIS = Number(process.env.MAX_ZIP_FETCH_TIME_MILLIS ?? String(30 * 1000)); // 30 seconds default. + const { files, outputType } = ZipResourcesInputSchema.parse(args); const zip = new JSZip(); + let remainingUploadBytes = MAX_ZIP_FETCH_SIZE; + const uploadEndTime = Date.now() + MAX_ZIP_FETCH_TIME_MILLIS; + for (const [fileName, urlString] of Object.entries(files)) { try { + if (remainingUploadBytes <= 0) { + throw new Error(`Max upload size of ${MAX_ZIP_FETCH_SIZE} bytes exceeded`); + } + const url = new URL(urlString); if (url.protocol !== 'http:' && url.protocol !== 'https:' && url.protocol !== 'data:') { throw new Error(`Unsupported URL protocol for ${urlString}. Only http, https, and data URLs are supported.`); } - const response = await fetch(url); - if (!response.ok) { - throw new Error(`Failed to fetch ${url}: ${response.statusText}`); - } - const arrayBuffer = await response.arrayBuffer(); - zip.file(fileName, arrayBuffer); + + const response = await fetchSafely(url, { + maxBytes: remainingUploadBytes, + timeoutMillis: uploadEndTime - Date.now() + }); + remainingUploadBytes -= response.byteLength; + + zip.file(fileName, response); } catch (error) { - throw new Error(`Error fetching file ${urlString}: ${error instanceof Error ? error.message : String(error)}`); + throw new Error( + `Error fetching file ${urlString}: ${error instanceof Error ? error.message : String(error)}` + ); } } @@ -1064,5 +1078,63 @@ export const createServer = () => { return { server, cleanup, startNotificationIntervals }; }; +/** + * Fetch a URL with limits on maximum bytes and timeout, to avoid getting overwhelmed by large or slow responses. + */ +async function fetchSafely(url: URL, {maxBytes, timeoutMillis}: {maxBytes: number, timeoutMillis: number}) { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(`Fetching ${url} took more than ${timeoutMillis} ms and was aborted.`), timeoutMillis); + + try { + const response = await fetch(url, { signal: controller.signal }); + if (!response.body) { + throw new Error('No response body'); + } + + // Note: we can't trust the Content-Length header: a malicious or clumsy server could return much more data than advertised. + // We check it here for early bail-out, but we still need to monitor actual bytes read below. + const contentLengthHeader = response.headers.get("content-length"); + if (contentLengthHeader != null) { + const contentLength = parseInt(contentLengthHeader, 10); + if (contentLength > maxBytes) { + throw new Error(`Content-Length for ${url} exceeds max of ${maxBytes}: ${contentLength}`); + } + } + + const reader = response.body.getReader(); + const chunks = []; + let totalSize = 0; + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + totalSize += value.length; + + if (totalSize > maxBytes) { + reader.cancel(); + throw new Error(`Response from ${url} exceeds ${maxBytes} bytes`); + } + + chunks.push(value); + } + } finally { + reader.releaseLock(); + } + + const buffer = new Uint8Array(totalSize); + let offset = 0; + for (const chunk of chunks) { + buffer.set(chunk, offset); + offset += chunk.length; + } + + return buffer.buffer; + } finally { + clearTimeout(timeout); + } +} + const MCP_TINY_IMAGE = "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"; From b801c5f7bf332298173d78190f1a6ddee8d2100a Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Tue, 14 Oct 2025 20:20:49 +0100 Subject: [PATCH 11/16] zip: default value for files --- src/everything/everything.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/everything/everything.ts b/src/everything/everything.ts index 1c55221af..5e9dd0862 100644 --- a/src/everything/everything.ts +++ b/src/everything/everything.ts @@ -131,7 +131,9 @@ const StructuredContentSchema = { }; const ZipResourcesInputSchema = z.object({ - files: z.record(z.string().url().describe("URL of the file to include in the zip")).describe("Mapping of file names to URLs to include in the zip"), + files: z.record(z.string().url().describe("URL of the file to include in the zip")).describe("Mapping of file names to URLs to include in the zip").default({ + "README.md": "https://raw.githubusercontent.com/modelcontextprotocol/servers/refs/heads/main/README.md", + }), outputType: z.enum([ 'resourceLink', 'resource' From c442246903894ab98200cdf7b097aa315a9d93d9 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Tue, 14 Oct 2025 20:21:07 +0100 Subject: [PATCH 12/16] zip: reuse existing resource mechanics --- src/everything/everything.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/everything/everything.ts b/src/everything/everything.ts index 5e9dd0862..fc6d1171c 100644 --- a/src/everything/everything.ts +++ b/src/everything/everything.ts @@ -340,8 +340,6 @@ export const createServer = () => { }; }); - const transientResources = new Map(); - server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const uri = request.params.uri; From 5f004caaea89f7a9d65a9e782e602622c482faf7 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Tue, 14 Oct 2025 20:26:30 +0100 Subject: [PATCH 13/16] zip: reuse existing resource mechanics --- src/everything/everything.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/everything/everything.ts b/src/everything/everything.ts index fc6d1171c..63ffb9cf7 100644 --- a/src/everything/everything.ts +++ b/src/everything/everything.ts @@ -343,12 +343,6 @@ export const createServer = () => { server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const uri = request.params.uri; - if (transientResources.has(uri)) { - return { - contents: [transientResources.get(uri)!], - }; - } - if (uri.startsWith("test://static/resource/")) { const index = parseInt(uri.split("/").pop() ?? "", 10) - 1; if (index >= 0 && index < ALL_RESOURCES.length) { @@ -905,7 +899,7 @@ export const createServer = () => { const blob = await zip.generateAsync({ type: "base64" }); const mimeType = "application/zip"; const name = `out_${Date.now()}.zip`; - const uri = `resource://${name}`; + const uri = `test://static/resource/${ALL_RESOURCES.length + 1}`; const resource = {uri, name, mimeType, blob}; if (outputType === 'resource') { return { @@ -915,7 +909,7 @@ export const createServer = () => { }] }; } else if (outputType === 'resourceLink') { - transientResources.set(uri, resource); + ALL_RESOURCES.push(resource); return { content: [{ type: "resource_link", From 25c538948e7e7864a8f27c9965cbbdba73b42b6a Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Tue, 14 Oct 2025 21:00:02 +0100 Subject: [PATCH 14/16] basic SSRF protection / ZIP_ALLOWED_DOMAINS --- src/everything/everything.ts | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/everything/everything.ts b/src/everything/everything.ts index 63ffb9cf7..7ff36b8a4 100644 --- a/src/everything/everything.ts +++ b/src/everything/everything.ts @@ -862,25 +862,36 @@ export const createServer = () => { } if (name === ToolName.ZIP_RESOURCES) { - const MAX_ZIP_FETCH_SIZE = Number(process.env.MAX_ZIP_FETCH_SIZE ?? String(10 * 1024 * 1024)); // 10 MB default - const MAX_ZIP_FETCH_TIME_MILLIS = Number(process.env.MAX_ZIP_FETCH_TIME_MILLIS ?? String(30 * 1000)); // 30 seconds default. + const ZIP_MAX_FETCH_SIZE = Number(process.env.ZIP_MAX_FETCH_SIZE ?? String(10 * 1024 * 1024)); // 10 MB default + const ZIP_MAX_FETCH_TIME_MILLIS = Number(process.env.ZIP_MAX_FETCH_TIME_MILLIS ?? String(30 * 1000)); // 30 seconds default. + // Comma-separated list of allowed domains. Empty means all domains are allowed. + const ZIP_ALLOWED_DOMAINS = (process.env.ZIP_ALLOWED_DOMAINS ?? "raw.githubusercontent.com").split(",").map(d => d.trim().toLowerCase()).filter(d => d.length > 0); const { files, outputType } = ZipResourcesInputSchema.parse(args); const zip = new JSZip(); - let remainingUploadBytes = MAX_ZIP_FETCH_SIZE; - const uploadEndTime = Date.now() + MAX_ZIP_FETCH_TIME_MILLIS; + let remainingUploadBytes = ZIP_MAX_FETCH_SIZE; + const uploadEndTime = Date.now() + ZIP_MAX_FETCH_TIME_MILLIS; for (const [fileName, urlString] of Object.entries(files)) { try { if (remainingUploadBytes <= 0) { - throw new Error(`Max upload size of ${MAX_ZIP_FETCH_SIZE} bytes exceeded`); + throw new Error(`Max upload size of ${ZIP_MAX_FETCH_SIZE} bytes exceeded`); } const url = new URL(urlString); if (url.protocol !== 'http:' && url.protocol !== 'https:' && url.protocol !== 'data:') { throw new Error(`Unsupported URL protocol for ${urlString}. Only http, https, and data URLs are supported.`); } + if (ZIP_ALLOWED_DOMAINS.length > 0 && (url.protocol === 'http:' || url.protocol === 'https:')) { + const domain = url.hostname; + const domainAllowed = ZIP_ALLOWED_DOMAINS.some(allowedDomain => { + return domain === allowedDomain || domain.endsWith(`.${allowedDomain}`); + }); + if (!domainAllowed) { + throw new Error(`Domain ${domain} is not in the allowed domains list.`); + } + } const response = await fetchSafely(url, { maxBytes: remainingUploadBytes, From a2ce06a670970226a8b10c69d7984eff4e95482f Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Tue, 14 Oct 2025 21:10:01 +0100 Subject: [PATCH 15/16] Replace zip tool with gzip for single file compression MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Changed tool from multi-file zip to single-file gzip compression - Replaced JSZip dependency with node:zlib (built-in) - Updated schema: renamed ZipResourcesInputSchema to GzipResourceInputSchema - Tool now accepts single file with name and data URI parameters - Updated tool name from ZIP_RESOURCES to GZIP_RESOURCE - Updated environment variables (ZIP_* to GZIP_*) - Removed jszip from package.json dependencies 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- package-lock.json | 100 -------------------------- src/everything/everything.ts | 133 ++++++++++++++++------------------- src/everything/package.json | 1 - 3 files changed, 62 insertions(+), 172 deletions(-) diff --git a/package-lock.json b/package-lock.json index 143dab7f2..0f706418a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2158,12 +2158,6 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "license": "MIT" - }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -2920,12 +2914,6 @@ "node": ">=0.10.0" } }, - "node_modules/immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", - "license": "MIT" - }, "node_modules/import-local": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", @@ -3056,12 +3044,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT" - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -4183,18 +4165,6 @@ "node": ">=6" } }, - "node_modules/jszip": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", - "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", - "license": "(MIT OR GPL-3.0-or-later)", - "dependencies": { - "lie": "~3.3.0", - "pako": "~1.0.2", - "readable-stream": "~2.3.6", - "setimmediate": "^1.0.5" - } - }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -4215,15 +4185,6 @@ "node": ">=6" } }, - "node_modules/lie": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "license": "MIT", - "dependencies": { - "immediate": "~3.0.5" - } - }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -4540,12 +4501,6 @@ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "license": "BlueOak-1.0.0" }, - "node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "license": "(MIT AND Zlib)" - }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -4704,12 +4659,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "license": "MIT" - }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -4816,27 +4765,6 @@ "dev": true, "license": "MIT" }, - "node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/readable-stream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, "node_modules/rechoir": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", @@ -5041,12 +4969,6 @@ "node": ">= 0.8.0" } }, - "node_modules/setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", - "license": "MIT" - }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -5248,21 +5170,6 @@ "node": ">= 0.8" } }, - "node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/string_decoder/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -5654,12 +5561,6 @@ "punycode": "^2.1.0" } }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -5930,7 +5831,6 @@ "@modelcontextprotocol/sdk": "^1.18.0", "cors": "^2.8.5", "express": "^4.21.1", - "jszip": "^3.10.1", "zod": "^3.23.8", "zod-to-json-schema": "^3.23.5" }, diff --git a/src/everything/everything.ts b/src/everything/everything.ts index 7ff36b8a4..5d3f540d5 100644 --- a/src/everything/everything.ts +++ b/src/everything/everything.ts @@ -30,7 +30,7 @@ import { zodToJsonSchema } from "zod-to-json-schema"; import { readFileSync } from "fs"; import { fileURLToPath } from "url"; import { dirname, join } from "path"; -import JSZip from "jszip"; +import { gzipSync } from "node:zlib"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -130,14 +130,13 @@ const StructuredContentSchema = { }) }; -const ZipResourcesInputSchema = z.object({ - files: z.record(z.string().url().describe("URL of the file to include in the zip")).describe("Mapping of file names to URLs to include in the zip").default({ - "README.md": "https://raw.githubusercontent.com/modelcontextprotocol/servers/refs/heads/main/README.md", - }), +const GzipResourceInputSchema = z.object({ + name: z.string().describe("Name of the file to compress"), + data: z.string().url().describe("URL or data URI of the file content to compress"), outputType: z.enum([ 'resourceLink', 'resource' - ]).default('resource').describe("How the resulting zip file should be returned. 'resourceLink' returns a link to a resource that can be read later, 'resource' returns a full resource object."), + ]).default('resource').describe("How the resulting gzipped file should be returned. 'resourceLink' returns a link to a resource that can be read later, 'resource' returns a full resource object."), }); enum ToolName { @@ -152,7 +151,7 @@ enum ToolName { ELICITATION = "startElicitation", GET_RESOURCE_LINKS = "getResourceLinks", STRUCTURED_CONTENT = "structuredContent", - ZIP_RESOURCES = "zip", + GZIP_RESOURCE = "gzip", LIST_ROOTS = "listRoots" } @@ -545,9 +544,9 @@ export const createServer = () => { outputSchema: zodToJsonSchema(StructuredContentSchema.output) as ToolOutput, }, { - name: ToolName.ZIP_RESOURCES, - description: "Compresses the provided resource files (mapping of name to URI, which can be a data URI) to a zip file, which it returns as a data URI resource link.", - inputSchema: zodToJsonSchema(ZipResourcesInputSchema) as ToolInput, + name: ToolName.GZIP_RESOURCE, + description: "Compresses a single file using gzip compression. Takes a file name and data URI, returns the compressed data as a gzipped resource.", + inputSchema: zodToJsonSchema(GzipResourceInputSchema) as ToolInput, } ]; if (clientCapabilities!.roots) tools.push ({ @@ -861,75 +860,67 @@ export const createServer = () => { }; } - if (name === ToolName.ZIP_RESOURCES) { - const ZIP_MAX_FETCH_SIZE = Number(process.env.ZIP_MAX_FETCH_SIZE ?? String(10 * 1024 * 1024)); // 10 MB default - const ZIP_MAX_FETCH_TIME_MILLIS = Number(process.env.ZIP_MAX_FETCH_TIME_MILLIS ?? String(30 * 1000)); // 30 seconds default. + if (name === ToolName.GZIP_RESOURCE) { + const GZIP_MAX_FETCH_SIZE = Number(process.env.GZIP_MAX_FETCH_SIZE ?? String(10 * 1024 * 1024)); // 10 MB default + const GZIP_MAX_FETCH_TIME_MILLIS = Number(process.env.GZIP_MAX_FETCH_TIME_MILLIS ?? String(30 * 1000)); // 30 seconds default. // Comma-separated list of allowed domains. Empty means all domains are allowed. - const ZIP_ALLOWED_DOMAINS = (process.env.ZIP_ALLOWED_DOMAINS ?? "raw.githubusercontent.com").split(",").map(d => d.trim().toLowerCase()).filter(d => d.length > 0); - - const { files, outputType } = ZipResourcesInputSchema.parse(args); - const zip = new JSZip(); + const GZIP_ALLOWED_DOMAINS = (process.env.GZIP_ALLOWED_DOMAINS ?? "raw.githubusercontent.com").split(",").map(d => d.trim().toLowerCase()).filter(d => d.length > 0); - let remainingUploadBytes = ZIP_MAX_FETCH_SIZE; - const uploadEndTime = Date.now() + ZIP_MAX_FETCH_TIME_MILLIS; + const { name: fileName, data: dataUri, outputType } = GzipResourceInputSchema.parse(args); - for (const [fileName, urlString] of Object.entries(files)) { - try { - if (remainingUploadBytes <= 0) { - throw new Error(`Max upload size of ${ZIP_MAX_FETCH_SIZE} bytes exceeded`); + try { + const url = new URL(dataUri); + if (url.protocol !== 'http:' && url.protocol !== 'https:' && url.protocol !== 'data:') { + throw new Error(`Unsupported URL protocol for ${dataUri}. Only http, https, and data URLs are supported.`); + } + if (GZIP_ALLOWED_DOMAINS.length > 0 && (url.protocol === 'http:' || url.protocol === 'https:')) { + const domain = url.hostname; + const domainAllowed = GZIP_ALLOWED_DOMAINS.some(allowedDomain => { + return domain === allowedDomain || domain.endsWith(`.${allowedDomain}`); + }); + if (!domainAllowed) { + throw new Error(`Domain ${domain} is not in the allowed domains list.`); } + } - const url = new URL(urlString); - if (url.protocol !== 'http:' && url.protocol !== 'https:' && url.protocol !== 'data:') { - throw new Error(`Unsupported URL protocol for ${urlString}. Only http, https, and data URLs are supported.`); - } - if (ZIP_ALLOWED_DOMAINS.length > 0 && (url.protocol === 'http:' || url.protocol === 'https:')) { - const domain = url.hostname; - const domainAllowed = ZIP_ALLOWED_DOMAINS.some(allowedDomain => { - return domain === allowedDomain || domain.endsWith(`.${allowedDomain}`); - }); - if (!domainAllowed) { - throw new Error(`Domain ${domain} is not in the allowed domains list.`); - } - } + const response = await fetchSafely(url, { + maxBytes: GZIP_MAX_FETCH_SIZE, + timeoutMillis: GZIP_MAX_FETCH_TIME_MILLIS + }); - const response = await fetchSafely(url, { - maxBytes: remainingUploadBytes, - timeoutMillis: uploadEndTime - Date.now() - }); - remainingUploadBytes -= response.byteLength; + // Compress the data using gzip + const inputBuffer = Buffer.from(response); + const compressedBuffer = gzipSync(inputBuffer); + const blob = compressedBuffer.toString("base64"); - zip.file(fileName, response); - } catch (error) { - throw new Error( - `Error fetching file ${urlString}: ${error instanceof Error ? error.message : String(error)}` - ); - } - } + const mimeType = "application/gzip"; + const gzipName = `${fileName}.gz`; + const uri = `test://static/resource/${ALL_RESOURCES.length + 1}`; + const resource = {uri, name: gzipName, mimeType, blob}; - const blob = await zip.generateAsync({ type: "base64" }); - const mimeType = "application/zip"; - const name = `out_${Date.now()}.zip`; - const uri = `test://static/resource/${ALL_RESOURCES.length + 1}`; - const resource = {uri, name, mimeType, blob}; - if (outputType === 'resource') { - return { - content: [{ - type: "resource", - resource - }] - }; - } else if (outputType === 'resourceLink') { - ALL_RESOURCES.push(resource); - return { - content: [{ - type: "resource_link", - mimeType, - uri - }] - }; - } else { - throw new Error(`Unknown outputType: ${outputType}`); + if (outputType === 'resource') { + return { + content: [{ + type: "resource", + resource + }] + }; + } else if (outputType === 'resourceLink') { + ALL_RESOURCES.push(resource); + return { + content: [{ + type: "resource_link", + mimeType, + uri + }] + }; + } else { + throw new Error(`Unknown outputType: ${outputType}`); + } + } catch (error) { + throw new Error( + `Error processing file ${dataUri}: ${error instanceof Error ? error.message : String(error)}` + ); } } diff --git a/src/everything/package.json b/src/everything/package.json index ca5964f90..e388922d1 100644 --- a/src/everything/package.json +++ b/src/everything/package.json @@ -25,7 +25,6 @@ "@modelcontextprotocol/sdk": "^1.18.0", "cors": "^2.8.5", "express": "^4.21.1", - "jszip": "^3.10.1", "zod": "^3.23.8", "zod-to-json-schema": "^3.23.5" }, From 036384d33c0534f986970d1d5a8200119bc04d38 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Tue, 14 Oct 2025 21:15:16 +0100 Subject: [PATCH 16/16] gzip --- src/everything/everything.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/everything/everything.ts b/src/everything/everything.ts index 5d3f540d5..90d275bfa 100644 --- a/src/everything/everything.ts +++ b/src/everything/everything.ts @@ -130,9 +130,9 @@ const StructuredContentSchema = { }) }; -const GzipResourceInputSchema = z.object({ - name: z.string().describe("Name of the file to compress"), - data: z.string().url().describe("URL or data URI of the file content to compress"), +const GzipInputSchema = z.object({ + name: z.string().describe("Name of the output file").default("README.md.gz"), + data: z.string().url().describe("URL or data URI of the file content to compress").default("https://raw.githubusercontent.com/modelcontextprotocol/servers/refs/heads/main/README.md"), outputType: z.enum([ 'resourceLink', 'resource' @@ -151,7 +151,7 @@ enum ToolName { ELICITATION = "startElicitation", GET_RESOURCE_LINKS = "getResourceLinks", STRUCTURED_CONTENT = "structuredContent", - GZIP_RESOURCE = "gzip", + GZIP = "gzip", LIST_ROOTS = "listRoots" } @@ -544,9 +544,9 @@ export const createServer = () => { outputSchema: zodToJsonSchema(StructuredContentSchema.output) as ToolOutput, }, { - name: ToolName.GZIP_RESOURCE, + name: ToolName.GZIP, description: "Compresses a single file using gzip compression. Takes a file name and data URI, returns the compressed data as a gzipped resource.", - inputSchema: zodToJsonSchema(GzipResourceInputSchema) as ToolInput, + inputSchema: zodToJsonSchema(GzipInputSchema) as ToolInput, } ]; if (clientCapabilities!.roots) tools.push ({ @@ -860,13 +860,13 @@ export const createServer = () => { }; } - if (name === ToolName.GZIP_RESOURCE) { + if (name === ToolName.GZIP) { const GZIP_MAX_FETCH_SIZE = Number(process.env.GZIP_MAX_FETCH_SIZE ?? String(10 * 1024 * 1024)); // 10 MB default const GZIP_MAX_FETCH_TIME_MILLIS = Number(process.env.GZIP_MAX_FETCH_TIME_MILLIS ?? String(30 * 1000)); // 30 seconds default. // Comma-separated list of allowed domains. Empty means all domains are allowed. const GZIP_ALLOWED_DOMAINS = (process.env.GZIP_ALLOWED_DOMAINS ?? "raw.githubusercontent.com").split(",").map(d => d.trim().toLowerCase()).filter(d => d.length > 0); - const { name: fileName, data: dataUri, outputType } = GzipResourceInputSchema.parse(args); + const { name, data: dataUri, outputType } = GzipInputSchema.parse(args); try { const url = new URL(dataUri); @@ -894,9 +894,8 @@ export const createServer = () => { const blob = compressedBuffer.toString("base64"); const mimeType = "application/gzip"; - const gzipName = `${fileName}.gz`; const uri = `test://static/resource/${ALL_RESOURCES.length + 1}`; - const resource = {uri, name: gzipName, mimeType, blob}; + const resource = {uri, name, mimeType, blob}; if (outputType === 'resource') { return {