@@ -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+
761822const 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