Skip to content

Commit 4e73646

Browse files
committed
gzip w/ no deps
1 parent 07709c9 commit 4e73646

File tree

2 files changed

+61
-73
lines changed

2 files changed

+61
-73
lines changed

mcp-server/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
"dotenv": "^16.4.7",
2222
"express": "^4.21.2",
2323
"express-rate-limit": "^8.0.1",
24-
"jszip": "^3.10.1",
2524
"raw-body": "^3.0.0"
2625
},
2726
"devDependencies": {

mcp-server/src/services/mcp.ts

Lines changed: 61 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
} from "@modelcontextprotocol/sdk/types.js";
2020
import { z } from "zod";
2121
import { zodToJsonSchema } from "zod-to-json-schema";
22-
import JSZip from "jszip";
22+
import { gzipSync } from "node:zlib";
2323

2424
type ToolInput = {
2525
type: "object";
@@ -80,14 +80,13 @@ const GetResourceReferenceSchema = z.object({
8080
.describe("ID of the resource to reference (1-100)"),
8181
});
8282

83-
const ZipResourcesInputSchema = z.object({
84-
files: z
85-
.record(z.string().url().describe("URL of the file to include in the zip"))
86-
.describe("Mapping of file names to URLs to include in the zip"),
83+
const GzipInputSchema = z.object({
84+
name: z.string().describe("Name of the output file").default("README.md.gz"),
85+
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"),
8786
outputType: z.enum([
8887
'resourceLink',
8988
'resource'
90-
]).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."),
89+
]).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."),
9190
});
9291

9392
enum ToolName {
@@ -98,7 +97,7 @@ enum ToolName {
9897
GET_TINY_IMAGE = "getTinyImage",
9998
ANNOTATED_MESSAGE = "annotatedMessage",
10099
GET_RESOURCE_REFERENCE = "getResourceReference",
101-
ZIP_RESOURCES = "zip",
100+
GZIP = "gzip",
102101
}
103102

104103
enum PromptName {
@@ -459,10 +458,9 @@ export const createMcpServer = (): McpServerWrapper => {
459458
inputSchema: zodToJsonSchema(GetResourceReferenceSchema) as ToolInput,
460459
},
461460
{
462-
name: ToolName.ZIP_RESOURCES,
463-
description:
464-
"Compresses the provided resource files (mapping of name to URI, which can be a data URI) to a zip file. Supports multiple output formats: inlined data URI (default), resource link, or full resource object",
465-
inputSchema: zodToJsonSchema(ZipResourcesInputSchema) as ToolInput,
461+
name: ToolName.GZIP,
462+
description: "Compresses a single file using gzip compression. Takes a file name and data URI, returns the compressed data as a gzipped resource.",
463+
inputSchema: zodToJsonSchema(GzipInputSchema) as ToolInput,
466464
},
467465
];
468466

@@ -642,75 +640,66 @@ export const createMcpServer = (): McpServerWrapper => {
642640
return { content };
643641
}
644642

645-
if (name === ToolName.ZIP_RESOURCES) {
646-
const ZIP_MAX_FETCH_SIZE = Number(process.env.ZIP_MAX_FETCH_SIZE ?? String(10 * 1024 * 1024)); // 10 MB default
647-
const ZIP_MAX_FETCH_TIME_MILLIS = Number(process.env.ZIP_MAX_FETCH_TIME_MILLIS ?? String(30 * 1000)); // 30 seconds default.
643+
if (name === ToolName.GZIP) {
644+
const GZIP_MAX_FETCH_SIZE = Number(process.env.GZIP_MAX_FETCH_SIZE ?? String(10 * 1024 * 1024)); // 10 MB default
645+
const GZIP_MAX_FETCH_TIME_MILLIS = Number(process.env.GZIP_MAX_FETCH_TIME_MILLIS ?? String(30 * 1000)); // 30 seconds default.
648646
// Comma-separated list of allowed domains. Empty means all domains are allowed.
649-
const ZIP_ALLOWED_DOMAINS = (process.env.ZIP_ALLOWED_DOMAINS ?? "raw.githubusercontent.com").split(",").map(d => d.trim().toLowerCase()).filter(d => d.length > 0);
650-
651-
const { files, outputType } = ZipResourcesInputSchema.parse(args);
652-
const zip = new JSZip();
647+
const GZIP_ALLOWED_DOMAINS = (process.env.GZIP_ALLOWED_DOMAINS ?? "raw.githubusercontent.com").split(",").map(d => d.trim().toLowerCase()).filter(d => d.length > 0);
653648

654-
let remainingUploadBytes = ZIP_MAX_FETCH_SIZE;
655-
const uploadEndTime = Date.now() + ZIP_MAX_FETCH_TIME_MILLIS;
649+
const { name, data: dataUri, outputType } = GzipInputSchema.parse(args);
656650

657-
for (const [fileName, urlString] of Object.entries(files)) {
658-
try {
659-
if (remainingUploadBytes <= 0) {
660-
throw new Error(`Max upload size of ${ZIP_MAX_FETCH_SIZE} bytes exceeded`);
651+
try {
652+
const url = new URL(dataUri);
653+
if (url.protocol !== 'http:' && url.protocol !== 'https:' && url.protocol !== 'data:') {
654+
throw new Error(`Unsupported URL protocol for ${dataUri}. Only http, https, and data URLs are supported.`);
655+
}
656+
if (GZIP_ALLOWED_DOMAINS.length > 0 && (url.protocol === 'http:' || url.protocol === 'https:')) {
657+
const domain = url.hostname;
658+
const domainAllowed = GZIP_ALLOWED_DOMAINS.some(allowedDomain => {
659+
return domain === allowedDomain || domain.endsWith(`.${allowedDomain}`);
660+
});
661+
if (!domainAllowed) {
662+
throw new Error(`Domain ${domain} is not in the allowed domains list.`);
661663
}
664+
}
662665

663-
const url = new URL(urlString);
664-
if (url.protocol !== 'http:' && url.protocol !== 'https:' && url.protocol !== 'data:') {
665-
throw new Error(`Unsupported URL protocol for ${urlString}. Only http, https, and data URLs are supported.`);
666-
}
667-
if (ZIP_ALLOWED_DOMAINS.length > 0 && (url.protocol === 'http:' || url.protocol === 'https:')) {
668-
const domain = url.hostname;
669-
const domainAllowed = ZIP_ALLOWED_DOMAINS.some(allowedDomain => {
670-
return domain === allowedDomain || domain.endsWith(`.${allowedDomain}`);
671-
});
672-
if (!domainAllowed) {
673-
throw new Error(`Domain ${domain} is not in the allowed domains list.`);
674-
}
675-
}
666+
const response = await fetchSafely(url, {
667+
maxBytes: GZIP_MAX_FETCH_SIZE,
668+
timeoutMillis: GZIP_MAX_FETCH_TIME_MILLIS
669+
});
676670

677-
const response = await fetchSafely(url, {
678-
maxBytes: remainingUploadBytes,
679-
timeoutMillis: uploadEndTime - Date.now()
680-
});
681-
remainingUploadBytes -= response.byteLength;
671+
// Compress the data using gzip
672+
const inputBuffer = Buffer.from(response);
673+
const compressedBuffer = gzipSync(inputBuffer);
674+
const blob = compressedBuffer.toString("base64");
682675

683-
zip.file(fileName, response);
684-
} catch (error) {
685-
throw new Error(
686-
`Error fetching file ${urlString}: ${error instanceof Error ? error.message : String(error)}`
687-
);
688-
}
689-
}
676+
const mimeType = "application/gzip";
677+
const uri = `test://static/resource/${ALL_RESOURCES.length + 1}`;
678+
const resource = <Resource>{uri, name, mimeType, blob};
690679

691-
const blob = await zip.generateAsync({ type: "base64" });
692-
const mimeType = "application/zip";
693-
const name = `out_${Date.now()}.zip`;
694-
const uri = `test://static/resource/${ALL_RESOURCES.length + 1}`;
695-
const resource = <Resource>{uri, name, mimeType, blob};
696-
if (outputType === 'resource') {
697-
return {
698-
content: [{
699-
type: "resource",
700-
resource
701-
}]
702-
};
703-
} else if (outputType === 'resourceLink') {
704-
ALL_RESOURCES.push(resource);
705-
return {
706-
content: [{
707-
type: "resource_link",
708-
mimeType,
709-
uri
710-
}]
711-
};
712-
} else {
713-
throw new Error(`Unknown outputType: ${outputType}`);
680+
if (outputType === 'resource') {
681+
return {
682+
content: [{
683+
type: "resource",
684+
resource
685+
}]
686+
};
687+
} else if (outputType === 'resourceLink') {
688+
ALL_RESOURCES.push(resource);
689+
return {
690+
content: [{
691+
type: "resource_link",
692+
mimeType,
693+
uri
694+
}]
695+
};
696+
} else {
697+
throw new Error(`Unknown outputType: ${outputType}`);
698+
}
699+
} catch (error) {
700+
throw new Error(
701+
`Error processing file ${dataUri}: ${error instanceof Error ? error.message : String(error)}`
702+
);
714703
}
715704
}
716705

0 commit comments

Comments
 (0)