From 2ae5dd3bdcb77290b06a5cd71756303a5a2e7de9 Mon Sep 17 00:00:00 2001 From: Artem Rozumenko Date: Tue, 7 Jul 2026 11:33:22 +0300 Subject: [PATCH] fix(web): proxy /mcp to /mcp/ upstream to break redirect loop (#352) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The remote MCP endpoint was unreachable (ConnectionRefused). The Starlette Mount at /mcp (streamable route at "/") 307-redirects the bare /mcp to /mcp/, and because the middleware rewrote to the raw upstream, that redirect leaked the internal Docker host (http://backend:8000/mcp/) to the client. Next's trailingSlash:false then 308-bounced /mcp/ back to /mcp, forming a loop. Proxy /mcp to the trailing-slash form the Mount expects so the backend never redirects. rewrite() keeps the client-facing URL as /mcp — no client-visible redirect, no loop, no leaked internal host. Co-Authored-By: Claude Opus 4.8 (1M context) --- web/src/middleware.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/web/src/middleware.ts b/web/src/middleware.ts index a9bd97f..5d8b9c9 100644 --- a/web/src/middleware.ts +++ b/web/src/middleware.ts @@ -29,8 +29,15 @@ export async function middleware(request: NextRequest) { const url = new URL(pathname + request.nextUrl.search, BACKEND_URL); return NextResponse.rewrite(url); } - if (pathname.startsWith('/mcp')) { - const url = new URL(pathname + request.nextUrl.search, BACKEND_URL); + if (pathname === '/mcp' || pathname.startsWith('/mcp/')) { + // FastMCP's streamable app is mounted at /mcp with its route at "/", so the + // backend expects the trailing-slash form "/mcp/". Proxy the bare "/mcp" to + // "/mcp/" here — otherwise the Starlette Mount 307-redirects to + // http://backend:8000/mcp/, leaking the internal upstream host to the client + // (which then can't resolve it → ConnectionRefused). rewrite() keeps the + // client-facing URL as /mcp, so there's no client-visible redirect or loop. + const path = pathname === '/mcp' ? '/mcp/' : pathname; + const url = new URL(path + request.nextUrl.search, BACKEND_URL); return NextResponse.rewrite(url); } if (pathname === '/health') {