diff --git a/src/everything/__tests__/tools.test.ts b/src/everything/__tests__/tools.test.ts index a50bbd6592..a0c6872780 100644 --- a/src/everything/__tests__/tools.test.ts +++ b/src/everything/__tests__/tools.test.ts @@ -1,3 +1,4 @@ +import http from 'node:http'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { registerEchoTool, EchoSchema } from '../tools/echo.js'; @@ -1217,5 +1218,66 @@ describe('Tools', () => { handler!({ name: 'test.gz', data: 'ftp://example.com/file.txt', outputType: 'resource' }) ).rejects.toThrow('Unsupported URL protocol'); }); + + it('should reject HTTP error responses instead of compressing the error page', async () => { + const mockServer = { + registerTool: vi.fn(), + registerResource: vi.fn(), + } as unknown as McpServer; + + let handler: Function | null = null; + (mockServer.registerTool as any).mockImplementation( + (name: string, config: any, h: Function) => { + handler = h; + } + ); + + registerGZipFileAsResourceTool(mockServer); + + const httpServer = http.createServer((req, res) => { + if (req.url === '/ok.txt') { + res.writeHead(200, { 'content-type': 'text/plain' }); + res.end('real file contents'); + return; + } + const status = req.url === '/boom.txt' ? 500 : 404; + res.writeHead(status, { 'content-type': 'text/plain' }); + res.end(`error body for ${status}`); + }); + await new Promise((resolve) => httpServer.listen(0, '127.0.0.1', resolve)); + const address = httpServer.address(); + if (!address || typeof address === 'string') { + throw new Error('expected a TCP address'); + } + + try { + const ok = await handler!({ + name: 'ok.txt.gz', + data: `http://127.0.0.1:${address.port}/ok.txt`, + outputType: 'resource', + }); + expect(ok.content[0].type).toBe('resource'); + + await expect( + handler!({ + name: 'missing.txt.gz', + data: `http://127.0.0.1:${address.port}/missing.txt`, + outputType: 'resource', + }) + ).rejects.toThrow('status 404'); + + await expect( + handler!({ + name: 'boom.txt.gz', + data: `http://127.0.0.1:${address.port}/boom.txt`, + outputType: 'resource', + }) + ).rejects.toThrow('status 500'); + } finally { + await new Promise((resolve, reject) => + httpServer.close((err) => (err ? reject(err) : resolve())) + ); + } + }); }); }); diff --git a/src/everything/tools/gzip-file-as-resource.ts b/src/everything/tools/gzip-file-as-resource.ts index 3dd6fdae4a..3127c1639b 100644 --- a/src/everything/tools/gzip-file-as-resource.ts +++ b/src/everything/tools/gzip-file-as-resource.ts @@ -193,6 +193,10 @@ async function fetchSafely( try { // Fetch the data const response = await fetch(url, { signal: controller.signal }); + if (!response.ok) { + await response.body?.cancel().catch(() => {}); + throw new Error(`Failed to fetch ${url}: status ${response.status}`); + } if (!response.body) { throw new Error("No response body"); }