Skip to content

Commit 1910aa1

Browse files
committed
zip: add fetch limits (size + time), simplify resource handling
1 parent a948ced commit 1910aa1

File tree

1 file changed

+80
-19
lines changed

1 file changed

+80
-19
lines changed

mcp-server/src/services/mcp.ts

Lines changed: 80 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,6 @@ export const createMcpServer = (): McpServerWrapper => {
130130
);
131131

132132
const subscriptions: Set<string> = new Set();
133-
const transientResources: Map<string, Resource> = new Map();
134133

135134
// Set up update interval for subscribed resources
136135
const subsUpdateInterval = setInterval(() => {
@@ -274,12 +273,6 @@ export const createMcpServer = (): McpServerWrapper => {
274273
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
275274
const uri = request.params.uri;
276275

277-
if (transientResources.has(uri)) {
278-
return {
279-
contents: [transientResources.get(uri)!],
280-
};
281-
}
282-
283276
if (uri.startsWith("test://static/resource/")) {
284277
const index = parseInt(uri.split("/").pop() ?? "", 10) - 1;
285278
if (index >= 0 && index < ALL_RESOURCES.length) {
@@ -650,23 +643,33 @@ export const createMcpServer = (): McpServerWrapper => {
650643
}
651644

652645
if (name === ToolName.ZIP_RESOURCES) {
646+
const MAX_ZIP_FETCH_SIZE = Number(process.env.MAX_ZIP_FETCH_SIZE ?? String(10 * 1024 * 1024)); // 10 MB default
647+
const MAX_ZIP_FETCH_TIME_MILLIS = Number(process.env.MAX_ZIP_FETCH_TIME_MILLIS ?? String(30 * 1000)); // 30 seconds default.
648+
653649
const { files, outputType } = ZipResourcesInputSchema.parse(args);
654650
const zip = new JSZip();
655651

652+
let remainingUploadBytes = MAX_ZIP_FETCH_SIZE;
653+
const uploadEndTime = Date.now() + MAX_ZIP_FETCH_TIME_MILLIS;
654+
656655
for (const [fileName, urlString] of Object.entries(files)) {
657656
try {
657+
if (remainingUploadBytes <= 0) {
658+
throw new Error(`Max upload size of ${MAX_ZIP_FETCH_SIZE} bytes exceeded`);
659+
}
660+
658661
const url = new URL(urlString);
659662
if (url.protocol !== 'http:' && url.protocol !== 'https:' && url.protocol !== 'data:') {
660663
throw new Error(`Unsupported URL protocol for ${urlString}. Only http, https, and data URLs are supported.`);
661664
}
662-
const response = await fetch(url);
663-
if (!response.ok) {
664-
throw new Error(
665-
`Failed to fetch ${url}: ${response.statusText}`
666-
);
667-
}
668-
const arrayBuffer = await response.arrayBuffer();
669-
zip.file(fileName, arrayBuffer);
665+
666+
const response = await fetchSafely(url, {
667+
maxBytes: remainingUploadBytes,
668+
timeoutMillis: uploadEndTime - Date.now()
669+
});
670+
remainingUploadBytes -= response.byteLength;
671+
672+
zip.file(fileName, response);
670673
} catch (error) {
671674
throw new Error(
672675
`Error fetching file ${urlString}: ${error instanceof Error ? error.message : String(error)}`
@@ -677,17 +680,17 @@ export const createMcpServer = (): McpServerWrapper => {
677680
const blob = await zip.generateAsync({ type: "base64" });
678681
const mimeType = "application/zip";
679682
const name = `out_${Date.now()}.zip`;
680-
const uri = `resource://${name}`;
681-
const resource: Resource = { uri, name, mimeType, blob };
682-
if (outputType === "resource") {
683+
const uri = `test://static/resource/${ALL_RESOURCES.length + 1}`;
684+
const resource = <Resource>{uri, name, mimeType, blob};
685+
if (outputType === 'resource') {
683686
return {
684687
content: [{
685688
type: "resource",
686689
resource
687690
}]
688691
};
689692
} else if (outputType === 'resourceLink') {
690-
transientResources.set(uri, resource);
693+
ALL_RESOURCES.push(resource);
691694
return {
692695
content: [{
693696
type: "resource_link",
@@ -758,5 +761,63 @@ export const createMcpServer = (): McpServerWrapper => {
758761
return { server, cleanup };
759762
};
760763

764+
/**
765+
* Fetch a URL with limits on maximum bytes and timeout, to avoid getting overwhelmed by large or slow responses.
766+
*/
767+
async function fetchSafely(url: URL, {maxBytes, timeoutMillis}: {maxBytes: number, timeoutMillis: number}) {
768+
const controller = new AbortController();
769+
const timeout = setTimeout(() => controller.abort(`Fetching ${url} took more than ${timeoutMillis} ms and was aborted.`), timeoutMillis);
770+
771+
try {
772+
const response = await fetch(url, { signal: controller.signal });
773+
if (!response.body) {
774+
throw new Error('No response body');
775+
}
776+
777+
// Note: we can't trust the Content-Length header: a malicious or clumsy server could return much more data than advertised.
778+
// We check it here for early bail-out, but we still need to monitor actual bytes read below.
779+
const contentLengthHeader = response.headers.get("content-length");
780+
if (contentLengthHeader != null) {
781+
const contentLength = parseInt(contentLengthHeader, 10);
782+
if (contentLength > maxBytes) {
783+
throw new Error(`Content-Length for ${url} exceeds max of ${maxBytes}: ${contentLength}`);
784+
}
785+
}
786+
787+
const reader = response.body.getReader();
788+
const chunks = [];
789+
let totalSize = 0;
790+
791+
try {
792+
while (true) {
793+
const { done, value } = await reader.read();
794+
if (done) break;
795+
796+
totalSize += value.length;
797+
798+
if (totalSize > maxBytes) {
799+
reader.cancel();
800+
throw new Error(`Response from ${url} exceeds ${maxBytes} bytes`);
801+
}
802+
803+
chunks.push(value);
804+
}
805+
} finally {
806+
reader.releaseLock();
807+
}
808+
809+
const buffer = new Uint8Array(totalSize);
810+
let offset = 0;
811+
for (const chunk of chunks) {
812+
buffer.set(chunk, offset);
813+
offset += chunk.length;
814+
}
815+
816+
return buffer.buffer;
817+
} finally {
818+
clearTimeout(timeout);
819+
}
820+
}
821+
761822
const MCP_TINY_IMAGE =
762823
"iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAKsGlDQ1BJQ0MgUHJvZmlsZQAASImVlwdUU+kSgOfe9JDQEiIgJfQmSCeAlBBaAAXpYCMkAUKJMRBU7MriClZURLCs6KqIgo0idizYFsWC3QVZBNR1sWDDlXeBQ9jdd9575805c+a7c+efmf+e/z9nLgCdKZDJMlF1gCxpjjwyyI8dn5DIJvUABRiY0kBdIMyWcSMiwgCTUft3+dgGyJC9YzuU69/f/1fREImzhQBIBMbJomxhFsbHMe0TyuQ5ALg9mN9kbo5siK9gzJRjDWL8ZIhTR7hviJOHGY8fjomO5GGsDUCmCQTyVACaKeZn5wpTsTw0f4ztpSKJFGPsGbyzsmaLMMbqgiUWI8N4KD8n+S95Uv+WM1mZUyBIVfLIXoaF7C/JlmUK5v+fn+N/S1amYrSGOaa0NHlwJGaxvpAHGbNDlSxNnhI+yhLRcPwwpymCY0ZZmM1LHGWRwD9UuTZzStgop0gC+co8OfzoURZnB0SNsnx2pLJWipzHHWWBfKyuIiNG6U8T85X589Ki40Y5VxI7ZZSzM6JCx2J4Sr9cEansXywN8hurG6jce1b2X/Yr4SvX5qRFByv3LhjrXyzljuXMjlf2JhL7B4zFxCjjZTl+ylqyzAhlvDgzSOnPzo1Srs3BDuTY2gjlN0wXhESMMoRBELAhBjIhB+QggECQgBTEOeJ5Q2cUeLNl8+WS1LQcNhe7ZWI2Xyq0m8B2tHd0Bhi6syNH4j1r+C4irGtjvhWVAF4nBgcHT475Qm4BHEkCoNaO+SxnAKh3A1w5JVTIc0d8Q9cJCEAFNWCCDhiACViCLTiCK3iCLwRACIRDNCTATBBCGmRhnc+FhbAMCqAI1sNmKIOdsBv2wyE4CvVwCs7DZbgOt+AePIZ26IJX0AcfYQBBEBJCRxiIDmKImCE2iCPCQbyRACQMiUQSkCQkFZEiCmQhsgIpQoqRMmQXUokcQU4g55GrSCvyEOlAepF3yFcUh9JQJqqPmqMTUQ7KRUPRaHQGmorOQfPQfHQtWopWoAfROvQ8eh29h7ajr9B+HOBUcCycEc4Wx8HxcOG4RFwKTo5bjCvEleAqcNW4Rlwz7g6uHfca9wVPxDPwbLwt3hMfjI/BC/Fz8Ivxq/Fl+P34OvxF/B18B74P/51AJ+gRbAgeBD4hnpBKmEsoIJQQ9hJqCZcI9whdhI9EIpFFtCC6EYOJCcR04gLiauJ2Yg3xHLGV2EnsJ5FIOiQbkhcpnCQg5ZAKSFtJB0lnSbdJXaTPZBWyIdmRHEhOJEvJy8kl5APkM+Tb5G7yAEWdYkbxoIRTRJT5lHWUPZRGyk1KF2WAqkG1oHpRo6np1GXUUmo19RL1CfW9ioqKsYq7ylQVicpSlVKVwypXVDpUvtA0adY0Hm06TUFbS9tHO0d7SHtPp9PN6b70RHoOfS29kn6B/oz+WZWhaqfKVxWpLlEtV61Tva36Ro2iZqbGVZuplqdWonZM7abaa3WKurk6T12gvli9XP2E+n31fg2GhoNGuEaWxmqNAxpXNXo0SZrmmgGaIs18zd2aFzQ7GTiGCYPHEDJWMPYwLjG6mESmBZPPTGcWMQ8xW5h9WppazlqxWvO0yrVOa7WzcCxzFp+VyVrHOspqY30dpz+OO048btW46nG3x33SHq/tqy3WLtSu0b6n/VWHrROgk6GzQade56kuXtdad6ruXN0dupd0X49njvccLxxfOP7o+Ed6qJ61XqTeAr3dejf0+vUN9IP0Zfpb9S/ovzZgGfgapBtsMjhj0GvIMPQ2lBhuMjxr+JKtxeayM9ml7IvsPiM9o2AjhdEuoxajAWML4xjj5cY1xk9NqCYckxSTTSZNJn2mhqaTTReaVpk+MqOYcczSzLaYNZt9MrcwjzNfaV5v3mOhbcG3yLOosnhiSbf0sZxjWWF514poxbHKsNpudcsatXaxTrMut75pg9q42khsttu0TiBMcJ8gnVAx4b4tzZZrm2tbZdthx7ILs1tuV2/3ZqLpxMSJGyY2T/xu72Kfab/H/rGDpkOIw3KHRod3jtaOQsdyx7tOdKdApyVODU5vnW2cxc47nB+4MFwmu6x0aXL509XNVe5a7drrZuqW5LbN7T6HyYngrOZccSe4+7kvcT/l/sXD1SPH46jHH562nhmeBzx7JllMEk/aM6nTy9hL4LXLq92b7Z3k/ZN3u4+Rj8Cnwue5r4mvyHevbzfXipvOPch942fvJ/er9fvE8+At4p3zx/kH+Rf6twRoBsQElAU8CzQOTA2sCuwLcglaEHQumBAcGrwh+D5fny/kV/L7QtxCFoVcDKWFRoWWhT4Psw6ThzVORieHTN44+ckUsynSKfXhEM4P3xj+NMIiYk7EyanEqRFTy6e+iHSIXBjZHMWImhV1IOpjtF/0uujHMZYxipimWLXY6bGVsZ/i/OOK49rjJ8Yvir+eoJsgSWhIJCXGJu5N7J8WMG3ztK7pLtMLprfNsJgxb8bVmbozM2eenqU2SzDrWBIhKS7pQNI3QbigQtCfzE/eltwn5Am3CF+JfEWbRL1iL3GxuDvFK6U4pSfVK3Vjam+aT1pJ2msJT1ImeZsenL4z/VNGeMa+jMHMuMyaLHJWUtYJqaY0Q3pxtsHsebNbZTayAln7HI85m+f0yUPle7OR7BnZDTlMbDi6obBU/KDoyPXOLc/9PDd27rF5GvOk827Mt56/an53XmDezwvwC4QLmhYaLVy2sGMRd9Guxcji5MVNS0yW5C/pWhq0dP8y6rKMZb8st19evPzDirgVjfn6+UvzO38I+qGqQLVAXnB/pefKnT/if5T82LLKadXWVd8LRYXXiuyLSoq+rRauvrbGYU3pmsG1KWtb1rmu27GeuF66vm2Dz4b9xRrFecWdGydvrNvE3lS46cPmWZuvljiX7NxC3aLY0l4aVtqw1XTr+q3fytLK7pX7ldds09u2atun7aLtt3f47qjeqb+zaOfXnyQ/PdgVtKuuwryiZDdxd+7uF3ti9zT/zPm5cq/u3qK9f+6T7mvfH7n/YqVbZeUBvQPrqtAqRVXvwekHbx3yP9RQbVu9q4ZVU3QYDisOvzySdKTtaOjRpmOcY9XHzY5vq2XUFtYhdfPr+urT6tsbEhpaT4ScaGr0bKw9aXdy3ymjU+WntU6vO0M9k39m8Gze2f5zsnOvz6ee72ya1fT4QvyFuxenXmy5FHrpyuXAyxeauc1nr3hdOXXV4+qJa5xr9dddr9fdcLlR+4vLL7Utri11N91uNtzyv9XYOqn1zG2f2+fv+N+5fJd/9/q9Kfda22LaHtyffr/9gehBz8PMh28f5T4aeLz0CeFJ4VP1pyXP9J5V/Gr1a027a/vpDv+OG8+jnj/uFHa++i37t29d+S/oL0q6Dbsrexx7TvUG9t56Oe1l1yvZq4HXBb9r/L7tjeWb43/4/nGjL76v66387eC71e913u/74PyhqT+i/9nHrI8Dnwo/63ze/4Xzpflr3NfugbnfSN9K/7T6s/F76Pcng1mDgzKBXDA8CuAwRVNSAN7tA6AnADCwGYI6bWSmHhZk5D9gmOA/8cjcPSyuANWYGRqNeOcADmNqvhRAzRdgaCyK9gXUyUmpo/Pv8Kw+JAbYv8K0HECi2x6tebQU/iEjc/xf+v6nBWXWv9l/AV0EC6JTIblRAAAAeGVYSWZNTQAqAAAACAAFARIAAwAAAAEAAQAAARoABQAAAAEAAABKARsABQAAAAEAAABSASgAAwAAAAEAAgAAh2kABAAAAAEAAABaAAAAAAAAAJAAAAABAAAAkAAAAAEAAqACAAQAAAABAAAAFKADAAQAAAABAAAAFAAAAAAXNii1AAAACXBIWXMAABYlAAAWJQFJUiTwAAAB82lUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNi4wLjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyI+CiAgICAgICAgIDx0aWZmOllSZXNvbHV0aW9uPjE0NDwvdGlmZjpZUmVzb2x1dGlvbj4KICAgICAgICAgPHRpZmY6T3JpZW50YXRpb24+MTwvdGlmZjpPcmllbnRhdGlvbj4KICAgICAgICAgPHRpZmY6WFJlc29sdXRpb24+MTQ0PC90aWZmOlhSZXNvbHV0aW9uPgogICAgICAgICA8dGlmZjpSZXNvbHV0aW9uVW5pdD4yPC90aWZmOlJlc29sdXRpb25Vbml0PgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KReh49gAAAjRJREFUOBGFlD2vMUEUx2clvoNCcW8hCqFAo1dKhEQpvsF9KrWEBh/ALbQ0KkInBI3SWyGPCCJEQliXgsTLefaca/bBWjvJzs6cOf/fnDkzOQJIjWm06/XKBEGgD8c6nU5VIWgBtQDPZPWtJE8O63a7LBgMMo/Hw0ql0jPjcY4RvmqXy4XMjUYDUwLtdhtmsxnYbDbI5/O0djqdFFKmsEiGZ9jP9gem0yn0ej2Yz+fg9XpfycimAD7DttstQTDKfr8Po9GIIg6Hw1Cr1RTgB+A72GAwgMPhQLBMJgNSXsFqtUI2myUo18pA6QJogefsPrLBX4QdCVatViklw+EQRFGEj88P2O12pEUGATmsXq+TaLPZ0AXgMRF2vMEqlQoJTSYTpNNpApvNZliv1/+BHDaZTAi2Wq1A3Ig0xmMej7+RcZjdbodUKkWAaDQK+GHjHPnImB88JrZIJAKFQgH2+z2BOczhcMiwRCIBgUAA+NN5BP6mj2DYff35gk6nA61WCzBn2JxO5wPM7/fLz4vD0E+OECfn8xl/0Gw2KbLxeAyLxQIsFgt8p75pDSO7h/HbpUWpewCike9WLpfB7XaDy+WCYrFI/slk8i0MnRRAUt46hPMI4vE4+Hw+ec7t9/44VgWigEeby+UgFArJWjUYOqhWG6x50rpcSfR6PVUfNOgEVRlTX0HhrZBKz4MZjUYWi8VoA+lc9H/VaRZYjBKrtXR8tlwumcFgeMWRbZpA9ORQWfVm8A/FsrLaxebd5wAAAABJRU5ErkJggg==";

0 commit comments

Comments
 (0)